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>
This commit is contained in:
Vonic 2026-04-14 23:28:14 +07:00
parent c1776b3ec8
commit 71edc183c4
5 changed files with 246 additions and 99 deletions

View file

@ -1,6 +1,6 @@
# Story 5.1: Giao diện Bảng giá & Lựa chọn Gói Cước (Pricing & Plan Selection UI)
Status: ready-for-dev
Status: done
## Story
@ -29,20 +29,20 @@ so that tôi biết chính xác số lượng file/tin nhắn mình nhận đư
## 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:
- [x] Task 1: Thiết kế lại Pricing Tiers (Frontend)
- [x] 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.
- [x] 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`.
- [x] Task 2: Kết nối nút CTA với Stripe Checkout (Frontend)
- [x] Subtask 2.1: Nút "Get Started" cho tier Free → redirect `/login` (giữ nguyên).
- [x] 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.
- [x] 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.
- [x] Task 3: Graceful degradation khi Offline
- [x] 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
@ -59,3 +59,35 @@ Mỗi tier subscription cần 1 Stripe Price ID (tạo trên Stripe Dashboard):
### 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?: () => void``disabled?: boolean`.
- Bật lại Monthly/Yearly toggle (đã bị comment), refactor để nhận `isYearly``onToggleBilling` props từ parent.
- Pro plan dùng `onAction``handleUpgradePro()`: 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
- [x] [Review][Decision] Gate Stripe checkout khi `isSelfHosted()` — DISMISSED: dự án chuyển sang SaaS-only, không còn self-hosted
- [x] [Review][Patch] Dùng `authenticatedFetch` thay `fetch` raw — xử lý 401/token refresh [pricing-section.tsx:43]
- [x] [Review][Patch] Hiện toast/error cho user khi Stripe checkout fail — hiện chỉ `console.error` [pricing-section.tsx:49-65]
- [x] [Review][Patch] Thêm `if (isLoading) return;` early guard chống double-click [pricing-section.tsx:33]
- [x] [Review][Patch] Xóa `useRouter` import không dùng [pricing-section.tsx:4,15]
- [x] [Review][Patch] Validate `checkout_url` bắt đầu bằng `https://` trước khi redirect [pricing-section.tsx:57]
- [x] [Review][Patch] Fix "Save 20%" → "Save 25%" hoặc tính dynamic từ giá [pricing.tsx:104]
- [x] [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.

View file

@ -4,3 +4,7 @@
- **stripe_subscription_id has no unique constraint** [surfsense_backend/app/db.py] — Column added without UNIQUE constraint. Should be enforced once Stripe integration (Epic 5) is implemented to prevent duplicate subscription mappings.
- **load_llm_config_from_yaml reads API keys directly from YAML file, not env vars** [surfsense_backend/app/config.py] — Pre-existing: YAML config stores API keys inline. Spec Task 1.2 says "đọc API keys từ env vars" but this is the existing pattern used throughout the project. To be refactored when security hardening is prioritized.
## Deferred from: code review of story 5-1 (2026-04-14)
- `ref` cast `as any` on Switch component in `pricing.tsx:99` — pre-existing issue, not introduced by this change. Should use proper `React.ComponentRef<typeof Switch>` type.

View file

@ -65,9 +65,9 @@ development_status:
4-2-graceful-degradation-offline-ui: done
4-3-global-network-sync-indicators: done
epic-4-retrospective: optional
epic-5: backlog
5-1-pricing-plan-selection-ui: backlog
5-2-stripe-payment-integration: backlog
5-3-stripe-webhook-sync: backlog
5-4-usage-tracking-rate-limit-enforcement: backlog
epic-5: in-progress
5-1-pricing-plan-selection-ui: done
5-2-stripe-payment-integration: ready-for-dev
5-3-stripe-webhook-sync: ready-for-dev
5-4-usage-tracking-rate-limit-enforcement: ready-for-dev
epic-5-retrospective: optional