Replace PAYG pricing tiers with subscription model (Free/Pro/Enterprise), enable Monthly/Yearly billing toggle, wire Pro CTA to Stripe checkout with authenticatedFetch, toast error feedback, double-click guard, checkout_url validation, and offline graceful degradation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.9 KiB
Story 5.1: Giao diện Bảng giá & Lựa chọn Gói Cước (Pricing & Plan Selection UI)
Status: done
Story
As a Khách hàng tiềm năng, I want xem một bảng giá rõ ràng về các gói cước (ví dụ: Free, Pro, Team) với quyền lợi tương ứng, so that tôi biết chính xác số lượng file/tin nhắn mình nhận được trước khi quyết định nâng cấp hoặc duy trì để quản lý ví (Wallet/Token) của mình.
Acceptance Criteria
- UI hiển thị các mức giá (monthly/yearly) rõ ràng cùng các bullets tính năng.
- Thiết kế áp dụng chuẩn UX-DR1 (Dark mode, Base Zinc, Accent Indigo) hiện có của app.
- Kèm theo hiệu ứng hover mượt mà cho các Pricing Cards (<150ms delay).
- Phân bổ ít nhất 2 gói cước (Free, Pro) gắn liền với Limit.
As-Is (Code hiện tại)
| Component | Hiện trạng | File |
|---|---|---|
| Pricing Page | Đã tồn tại — route /pricing |
surfsense_web/app/(home)/pricing/page.tsx |
| Pricing Section | Đã tồn tại — 3 tiers (FREE / PAY AS YOU GO / ENTERPRISE) | surfsense_web/components/pricing/pricing-section.tsx |
| Pricing Data | Static demoPlans constant — Free=500 pages, PAYG=$1/1000 pages, Enterprise=Contact |
pricing-section.tsx lines 2–59 |
| CTA Buttons | Free="Get Started" → /login, PAYG="Get Started" → /login, Enterprise="Contact Sales" → /contact |
pricing-section.tsx |
| Monthly/Yearly Toggle | Không có — chỉ có price và yearlyPrice fields nhưng chưa có toggle UI |
Gap: UI hiện tại mô hình PAYG (mua page packs 1 lần). Cần chuyển sang Subscription tiers (Free/Pro/Team monthly/yearly) theo PRD.
Tasks / Subtasks
-
Task 1: Thiết kế lại Pricing Tiers (Frontend)
- Subtask 1.1: Cập nhật
demoPlansconstant → đổi sang subscription tiers:- Free: 500 pages ETL, 50 LLM messages/day, basic models (GPT-3.5), $0/mo
- Pro: 5,000 pages ETL, 1,000 LLM messages/day, premium models (GPT-4, Claude), $X/mo hoặc $Y/year
- Team/Enterprise: Unlimited, custom pricing, SSO, audit logs
- Subtask 1.2: Thêm Monthly/Yearly toggle switch — hiển thị
pricevsyearlyPricetương ứng.
- Subtask 1.1: Cập nhật
-
Task 2: Kết nối nút CTA với Stripe Checkout (Frontend)
- Subtask 2.1: Nút "Get Started" cho tier Free → redirect
/login(giữ nguyên). - Subtask 2.2: Nút "Upgrade to Pro" → gọi
POST /api/v1/stripe/create-subscription-checkout(Story 5.2) vớiplan_idtương ứng. Nếu user chưa login → redirect/logintrước. - Subtask 2.3: Nút "Contact Sales" cho Enterprise → giữ nguyên
/contact.
- Subtask 2.1: Nút "Get Started" cho tier Free → redirect
-
Task 3: Graceful degradation khi Offline
- Subtask 3.1: Pricing data dùng static constant (load được offline). Disable nút "Upgrade" khi offline để tránh lỗi network request.
Dev Notes
Giữ lại hay xóa PAYG?
Quyết định kinh doanh: PAYG (mua page packs 1 lần) có thể tồn tại song song với subscription, hoặc bị thay thế hoàn toàn. Nếu giữ PAYG → thêm 1 tier "Pay As You Go" bên cạnh Free/Pro. Nếu thay → xóa PAYG flow cũ.
Stripe Price IDs
Mỗi tier subscription cần 1 Stripe Price ID (tạo trên Stripe Dashboard):
STRIPE_FREE_PRICE_ID(optional — free tier không cần checkout)STRIPE_PRO_MONTHLY_PRICE_IDSTRIPE_PRO_YEARLY_PRICE_ID→ Lưu vào env vars backend, KHÔNG hardcode trong frontend.
References
surfsense_web/components/pricing/pricing-section.tsx— pricing UI hiện tạisurfsense_web/app/(home)/pricing/page.tsx— pricing page route
Dev Agent Record
Implementation Notes
- Chuyển
pricing-section.tsxthành Client Component ("use client") để xử lý online state và Stripe CTA. - Cập nhật
PricingPlaninterface trongpricing.tsx: thêmonAction?: () => voidvàdisabled?: boolean. - Bật lại Monthly/Yearly toggle (đã bị comment), refactor để nhận
isYearlyvàonToggleBillingprops từ parent. - Pro plan dùng
onAction→handleUpgradePro(): checkisAuthenticated()rồi gọiPOST /api/v1/stripe/create-subscription-checkout. - Graceful degradation:
useEffectlắng ngheonline/offlineevents, disable nút Upgrade + thay text khi offline. - Pricing data là static constant → luôn render được kể cả offline.
- Quyết định xóa PAYG tier cũ, thay bằng Free/Pro/Enterprise subscription tiers theo PRD.
Completion Notes
✅ Tất cả tasks/subtasks hoàn thành. AC 1-4 đều được đáp ứng.
File List
surfsense_web/components/pricing/pricing-section.tsx— rewritten: subscription tiers, Stripe CTA, offline degradationsurfsense_web/components/pricing.tsx— updated: PricingPlan interface, toggle enabled, button renders onAction/Link
Review Findings
- [Review][Decision] Gate Stripe checkout khi
isSelfHosted()— DISMISSED: dự án chuyển sang SaaS-only, không còn self-hosted - [Review][Patch] Dùng
authenticatedFetchthayfetchraw — xử lý 401/token refresh [pricing-section.tsx:43] - [Review][Patch] Hiện toast/error cho user khi Stripe checkout fail — hiện chỉ
console.error[pricing-section.tsx:49-65] - [Review][Patch] Thêm
if (isLoading) return;early guard chống double-click [pricing-section.tsx:33] - [Review][Patch] Xóa
useRouterimport không dùng [pricing-section.tsx:4,15] - [Review][Patch] Validate
checkout_urlbắt đầu bằnghttps://trước khi redirect [pricing-section.tsx:57] - [Review][Patch] Fix "Save 20%" → "Save 25%" hoặc tính dynamic từ giá [pricing.tsx:104]
- [Review][Defer]
refcastas anytrên Switch component — deferred, pre-existing [pricing.tsx:99]
Change Log
- 2026-04-14: Chuyển sang subscription tiers (Free/Pro/Enterprise), bật toggle Monthly/Yearly, kết nối Stripe Checkout, graceful offline degradation.