feat: add pricing checkout flow, MFA type fixes, and Wave 13 audit docs

- Pricing page: enhanced with checkout modal integration, plan
  comparison table, and subscription funnel
- Payment return page: new VNPay/MoMo callback handler
- Subscription components: new checkout-modal with payment method
  selection (VNPay, MoMo, ZaloPay)
- API modules: type-safe PII encryption, improved error handling in
  MFA/auth/payments/analytics/search/notifications modules
- Audit docs: comprehensive Wave 13 platform assessment, pricing
  audit, production readiness checklist
- Updated PROJECT_TRACKER with Wave 13 status

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-12 20:17:11 +07:00
parent 51c4ecbf4e
commit db7147a95d
66 changed files with 6530 additions and 283 deletions

View File

@@ -3,6 +3,7 @@
import { Check, Crown, Rocket, Shield, X, Zap } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { CheckoutModal } from '@/components/subscription/checkout-modal';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@@ -13,8 +14,9 @@ import {
CardTitle,
} from '@/components/ui/card';
import { Link } from '@/i18n/navigation';
import { useAuthStore } from '@/lib/auth-store';
import { formatVND } from '@/lib/currency';
import { usePlans } from '@/lib/hooks/use-subscription';
import { usePlans, useBillingHistory } from '@/lib/hooks/use-subscription';
import type { PlanDto } from '@/lib/subscription-api';
import { cn } from '@/lib/utils';
@@ -197,10 +199,16 @@ function getFeatureValue(
export default function PricingPage() {
const t = useTranslations('pricing');
const { data: plansData, isLoading, error } = usePlans();
const { isAuthenticated } = useAuthStore();
const { data: billing } = useBillingHistory();
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>(
'monthly',
);
// Checkout modal state
const [checkoutPlan, setCheckoutPlan] = useState<PlanDto | null>(null);
const [checkoutOpen, setCheckoutOpen] = useState(false);
const plans = (plansData ?? (error ? FALLBACK_PLANS : []))
.slice()
.sort(
@@ -208,6 +216,51 @@ export default function PricingPage() {
PLAN_TIER_ORDER.indexOf(a.tier) - PLAN_TIER_ORDER.indexOf(b.tier),
);
// Current subscription info for logged-in users
const currentTier = billing?.subscription?.planTier ?? 'FREE';
const currentTierIndex = PLAN_TIER_ORDER.indexOf(currentTier);
const handleSelectPlan = (plan: PlanDto) => {
if (plan.tier === 'ENTERPRISE') return; // Enterprise uses contact form
if (plan.tier === 'FREE') return; // Free doesn't need payment
if (!isAuthenticated) {
// Redirect to register with plan context
return;
}
setCheckoutPlan(plan);
setCheckoutOpen(true);
};
const getPlanCta = (plan: PlanDto) => {
const tierIndex = PLAN_TIER_ORDER.indexOf(plan.tier);
if (plan.tier === 'FREE') {
if (isAuthenticated && currentTier === 'FREE') {
return { label: t('ctaCurrentPlan'), disabled: true, variant: 'outline' as const };
}
return { label: t('ctaFree'), disabled: false, variant: 'outline' as const };
}
if (plan.tier === 'ENTERPRISE') {
return { label: t('ctaEnterprise'), disabled: false, variant: 'outline' as const };
}
if (isAuthenticated) {
if (plan.tier === currentTier) {
return { label: t('ctaCurrentPlan'), disabled: true, variant: 'outline' as const };
}
if (tierIndex > currentTierIndex) {
return { label: t('ctaUpgrade'), disabled: false, variant: 'default' as const };
}
// Downgrade not supported from pricing page
return { label: t('ctaDowngrade'), disabled: true, variant: 'outline' as const };
}
return { label: t('ctaUpgrade'), disabled: false, variant: 'default' as const };
};
return (
<div className="bg-background">
{/* Hero section */}
@@ -223,6 +276,17 @@ export default function PricingPage() {
{t('subtitle')}
</p>
{/* Current plan indicator for logged-in users */}
{isAuthenticated && billing?.subscription && (
<div className="mx-auto mt-4 max-w-md">
<Badge variant="default" className="px-3 py-1 text-sm">
{t('currentPlanBadge', {
plan: t(`tiers.${currentTier}`),
})}
</Badge>
</div>
)}
{/* Billing cycle toggle */}
<div className="mt-8 flex items-center justify-center gap-3">
<Button
@@ -256,17 +320,21 @@ export default function PricingPage() {
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{plans.map((plan) => {
const isPopular = plan.tier === 'AGENT_PRO';
const isCurrent = isAuthenticated && plan.tier === currentTier;
const price =
billingCycle === 'monthly'
? plan.priceMonthlyVND
: plan.priceYearlyVND;
const cta = getPlanCta(plan);
return (
<Card
key={plan.id}
className={cn(
'relative flex flex-col transition-shadow hover:shadow-lg',
isPopular && 'border-primary shadow-md ring-1 ring-primary',
isCurrent && !isPopular && 'border-green-500 ring-1 ring-green-500',
)}
>
{isPopular && (
@@ -276,6 +344,13 @@ export default function PricingPage() {
</Badge>
</div>
)}
{isCurrent && (
<div className="absolute -top-3 right-4">
<Badge className="bg-green-600 text-white">
{t('currentPlan')}
</Badge>
</div>
)}
<CardHeader className="pb-2">
<div
className={cn(
@@ -378,19 +453,47 @@ export default function PricingPage() {
</ul>
{/* CTA */}
<Link href={'/register' as const} className="w-full">
{plan.tier === 'FREE' && !isAuthenticated ? (
<Link href={'/register' as const} className="w-full">
<Button
className="w-full"
variant="outline"
size="lg"
>
{t('ctaFree')}
</Button>
</Link>
) : plan.tier === 'ENTERPRISE' ? (
<Link href={'/' as const} className="w-full">
<Button
className="w-full"
variant="outline"
size="lg"
>
{t('ctaEnterprise')}
</Button>
</Link>
) : !isAuthenticated ? (
<Link href={'/register' as const} className="w-full">
<Button
className="w-full"
variant={isPopular ? 'default' : 'outline'}
size="lg"
>
{t('ctaUpgrade')}
</Button>
</Link>
) : (
<Button
className="w-full"
variant={isPopular ? 'default' : 'outline'}
variant={cta.variant}
size="lg"
disabled={cta.disabled}
onClick={() => handleSelectPlan(plan)}
>
{plan.tier === 'FREE'
? t('ctaFree')
: plan.tier === 'ENTERPRISE'
? t('ctaEnterprise')
: t('ctaUpgrade')}
{cta.label}
</Button>
</Link>
)}
</CardContent>
</Card>
);
@@ -422,9 +525,15 @@ export default function PricingPage() {
className={cn(
'px-4 py-3 text-center text-sm font-semibold',
plan.tier === 'AGENT_PRO' && 'bg-primary/5',
isAuthenticated && plan.tier === currentTier && 'bg-green-50',
)}
>
{t(`tiers.${plan.tier}`)}
<span>{t(`tiers.${plan.tier}`)}</span>
{isAuthenticated && plan.tier === currentTier && (
<Badge variant="secondary" className="ml-1 text-[10px]">
{t('currentPlan')}
</Badge>
)}
</th>
))}
</tr>
@@ -443,6 +552,7 @@ export default function PricingPage() {
className={cn(
'px-4 py-3 text-center text-sm',
plan.tier === 'AGENT_PRO' && 'bg-primary/5',
isAuthenticated && plan.tier === currentTier && 'bg-green-50',
)}
>
{typeof val === 'boolean' ? (
@@ -473,9 +583,15 @@ export default function PricingPage() {
{t('ctaDescription')}
</p>
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
<Link href={'/register' as const}>
<Button size="lg">{t('ctaRegister')}</Button>
</Link>
{isAuthenticated ? (
<Link href={'/dashboard/subscription' as const}>
<Button size="lg">{t('ctaManageSubscription')}</Button>
</Link>
) : (
<Link href={'/register' as const}>
<Button size="lg">{t('ctaRegister')}</Button>
</Link>
)}
<Link href={'/' as const}>
<Button variant="outline" size="lg">
{t('ctaLearnMore')}
@@ -484,6 +600,16 @@ export default function PricingPage() {
</div>
</div>
</section>
{/* Checkout modal */}
<CheckoutModal
open={checkoutOpen}
onOpenChange={setCheckoutOpen}
plan={checkoutPlan}
billingCycle={billingCycle}
isUpgrade={isAuthenticated && currentTierIndex > 0}
currentTier={currentTier}
/>
</div>
);
}