SurfSense/_bmad-output/implementation-artifacts/5-1-pricing-plan-selection-ui.md
Vonic 71edc183c4 feat(story-5.1): add subscription pricing UI with Stripe checkout integration
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>
2026-04-14 23:28:14 +07:00

5.9 KiB
Raw Blame History

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

  1. UI hiển thị các mức giá (monthly/yearly) rõ ràng cùng các bullets tính năng.
  2. Thiết kế áp dụng chuẩn UX-DR1 (Dark mode, Base Zinc, Accent Indigo) hiện có của app.
  3. Kèm theo hiệu ứng hover mượt mà cho các Pricing Cards (<150ms delay).
  4. 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 259
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ó priceyearlyPrice 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 demoPlans constant → đổ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ị price vs yearlyPrice tương ứng.
  • 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ới plan_id tương ứng. Nếu user chưa login → redirect /login trước.
    • Subtask 2.3: Nút "Contact Sales" cho Enterprise → giữ nguyên /contact.
  • 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_ID
  • STRIPE_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ại
  • surfsense_web/app/(home)/pricing/page.tsx — pricing page route

Dev Agent Record

Implementation Notes

  • Chuyển pricing-section.tsx thành Client Component ("use client") để xử lý online state và Stripe CTA.
  • Cập nhật PricingPlan interface trong pricing.tsx: thêm onAction?: () => voiddisabled?: boolean.
  • Bật lại Monthly/Yearly toggle (đã bị comment), refactor để nhận isYearlyonToggleBilling props từ parent.
  • Pro plan dùng onActionhandleUpgradePro(): check isAuthenticated() rồi gọi POST /api/v1/stripe/create-subscription-checkout.
  • Graceful degradation: useEffect lắng nghe online/offline events, 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 degradation
  • surfsense_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 authenticatedFetch thay fetch raw — 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 useRouter import không dùng [pricing-section.tsx:4,15]
  • [Review][Patch] Validate checkout_url bắt đầu bằng https:// 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] ref cast as any trê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.