'use client' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import { formatCurrency, PLAN_PRICES } from '@rentaldrivego/types' import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api' import { useDashboardI18n } from '@/components/I18nProvider' type Plan = 'STARTER' | 'GROWTH' | 'PRO' type BillingPeriod = 'MONTHLY' | 'ANNUAL' interface Subscription { id: string plan: Plan billingPeriod: BillingPeriod status: string currency: string trialEndAt: string | null currentPeriodEnd: string | null cancelAtPeriodEnd: boolean } interface Invoice { id: string amount: number currency: string status: string paymentProvider: string paidAt: string | null createdAt: string } interface ProviderAvailability { amanpay: boolean paypal: boolean } interface PlanFeature { id: string plan: Plan label: string sortOrder: number } interface EmployeeProfile { role?: string } const STATUS_BADGE: Record = { TRIALING: 'bg-sky-100 text-sky-700', ACTIVE: 'bg-green-100 text-green-700', PAST_DUE: 'bg-orange-100 text-orange-700', CANCELLED: 'bg-slate-100 text-slate-600', CANCELED: 'bg-slate-100 text-slate-600', UNPAID: 'bg-red-100 text-red-700', EXPIRED: 'bg-red-100 text-red-700', SUSPENDED: 'bg-red-100 text-red-700', } const INVOICE_STATUS: Record = { PAID: 'bg-green-100 text-green-700', PENDING: 'bg-orange-100 text-orange-700', FAILED: 'bg-red-100 text-red-700', REFUNDED: 'bg-slate-100 text-slate-600', } const PLANS: Plan[] = ['STARTER', 'GROWTH', 'PRO'] export default function SubscriptionPage() { const router = useRouter() const { language } = useDashboardI18n() const [subscription, setSubscription] = useState(null) const [invoices, setInvoices] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [canViewPage, setCanViewPage] = useState(null) const [verificationError, setVerificationError] = useState(null) const [verificationAttempt, setVerificationAttempt] = useState(0) const [selectedPlan, setSelectedPlan] = useState('STARTER') const [billingPeriod, setBillingPeriod] = useState('MONTHLY') const currency = 'MAD' const [provider, setProvider] = useState<'AMANPAY' | 'PAYPAL'>('AMANPAY') const [providerAvailability, setProviderAvailability] = useState({ amanpay: false, paypal: false }) const [planPrices, setPlanPrices] = useState>>>(PLAN_PRICES) const [planFeaturesList, setPlanFeaturesList] = useState([]) const [paying, setPaying] = useState(false) const [cancelling, setCancelling] = useState(false) const copy = { en: { title: 'Subscription', subtitle: 'Manage your plan, payment provider, and subscription invoices.', trial: 'Free trial', remaining: 'remaining. Subscribe before it ends to keep access.', currentPlan: 'Current plan', renews: 'renews', cancelScheduled: 'Cancellation scheduled at end of billing period.', undo: 'Undo', cancelling: 'Cancelling…', cancelPlan: 'Cancel plan', changePlan: 'Change plan', subscribe: 'Subscribe', selectPlan: 'Select a plan and payment provider to proceed.', monthly: 'Monthly', annual: 'Annual (save 20%)', active: 'Active', perMonthShort: 'mo', perYearShort: 'yr', paymentProvider: 'Payment provider', total: 'Total', perMonth: 'month', perYear: 'year', redirecting: 'Redirecting…', subscribeNow: 'Subscribe now', invoiceHistory: 'Invoice history', date: 'Date', provider: 'Provider', status: 'Status', paid: 'Paid', amount: 'Amount', loading: 'Loading…', verifying: 'Verifying access…', accessDenied: 'Access denied', accessDeniedBody: 'Only account owners can manage subscriptions.', retry: 'Retry', accessUnavailable: 'Unable to verify your access right now. Please try again.', noInvoices: 'No invoices yet.', noProviderConfigured: 'No payment provider is configured. Contact support to enable AmanPay or PayPal.', providerUnavailable: 'This payment provider is not configured.', statusLabels: { TRIALING: 'Trialing', ACTIVE: 'Active', PAST_DUE: 'Past due', CANCELLED: 'Cancelled', CANCELED: 'Canceled', UNPAID: 'Unpaid', EXPIRED: 'Expired', SUSPENDED: 'Suspended' } as Record, invoiceStatusLabels: { PAID: 'Paid', PENDING: 'Pending', FAILED: 'Failed', REFUNDED: 'Refunded' } as Record, planFeatures: { STARTER: ['Up to 10 vehicles', '1 user seat', 'Basic analytics', 'Carplace listing'], GROWTH: ['Up to 50 vehicles', '5 user seats', 'Full analytics', 'Priority listing', 'Custom branding'], PRO: ['Unlimited vehicles', 'Unlimited seats', 'Advanced reports', 'API access', 'Dedicated support'], } as Record, }, fr: { title: 'Abonnement', subtitle: 'Gérez votre plan, le prestataire de paiement et les factures d’abonnement.', trial: 'Essai gratuit', remaining: 'restants. Abonnez-vous avant la fin pour garder l’accès.', currentPlan: 'Plan actuel', renews: 'renouvelle le', cancelScheduled: 'Annulation programmée à la fin de la période.', undo: 'Annuler', cancelling: 'Annulation…', cancelPlan: 'Annuler le plan', changePlan: 'Changer de plan', subscribe: 'S’abonner', selectPlan: 'Sélectionnez un plan et un prestataire de paiement.', monthly: 'Mensuel', annual: 'Annuel (économie 20%)', active: 'Actif', perMonthShort: 'mois', perYearShort: 'an', paymentProvider: 'Prestataire de paiement', total: 'Total', perMonth: 'mois', perYear: 'an', redirecting: 'Redirection…', subscribeNow: 'S’abonner maintenant', invoiceHistory: 'Historique des factures', date: 'Date', provider: 'Prestataire', status: 'Statut', paid: 'Payé', amount: 'Montant', loading: 'Chargement…', verifying: 'Vérification de l’accès…', accessDenied: 'Accès refusé', accessDeniedBody: 'Seuls les propriétaires du compte peuvent gérer les abonnements.', retry: 'Réessayer', accessUnavailable: 'Impossible de vérifier votre accès pour le moment. Veuillez réessayer.', noInvoices: 'Aucune facture pour le moment.', noProviderConfigured: 'Aucun prestataire de paiement n’est configuré. Contactez le support pour activer AmanPay ou PayPal.', providerUnavailable: 'Ce prestataire de paiement n’est pas configuré.', statusLabels: { TRIALING: 'Essai', ACTIVE: 'Actif', PAST_DUE: 'En retard', CANCELLED: 'Annulé', CANCELED: 'Annulé', UNPAID: 'Impayé', EXPIRED: 'Expiré', SUSPENDED: 'Suspendu' } as Record, invoiceStatusLabels: { PAID: 'Payé', PENDING: 'En attente', FAILED: 'Échec', REFUNDED: 'Remboursé' } as Record, planFeatures: { STARTER: ['Jusqu’à 10 véhicules', '1 utilisateur', 'Analyses de base', 'Présence sur Carplace'], GROWTH: ['Jusqu’à 50 véhicules', '5 utilisateurs', 'Analyses complètes', 'Mise en avant prioritaire', 'Personnalisation'], PRO: ['Véhicules illimités', 'Utilisateurs illimités', 'Rapports avancés', 'Accès API', 'Support dédié'], } as Record, }, ar: { title: 'الاشتراك', subtitle: 'إدارة الخطة ومزوّد الدفع وفواتير الاشتراك.', trial: 'تجربة مجانية', remaining: 'متبقية. اشترك قبل انتهائها للحفاظ على الوصول.', currentPlan: 'الخطة الحالية', renews: 'يتجدد في', cancelScheduled: 'تمت جدولة الإلغاء عند نهاية فترة الفوترة.', undo: 'تراجع', cancelling: 'جارٍ الإلغاء…', cancelPlan: 'إلغاء الخطة', changePlan: 'تغيير الخطة', subscribe: 'اشتراك', selectPlan: 'اختر خطة ومزوّد دفع للمتابعة.', monthly: 'شهري', annual: 'سنوي (توفير 20%)', active: 'نشط', perMonthShort: 'شهر', perYearShort: 'سنة', paymentProvider: 'مزوّد الدفع', total: 'الإجمالي', perMonth: 'شهر', perYear: 'سنة', redirecting: 'جارٍ التحويل…', subscribeNow: 'اشترك الآن', invoiceHistory: 'سجل الفواتير', date: 'التاريخ', provider: 'المزوّد', status: 'الحالة', paid: 'مدفوع', amount: 'المبلغ', loading: 'جارٍ التحميل…', verifying: 'جارٍ التحقق من الوصول…', accessDenied: 'تم رفض الوصول', accessDeniedBody: 'يمكن لمالكي الحساب فقط إدارة الاشتراكات.', retry: 'إعادة المحاولة', accessUnavailable: 'تعذر التحقق من وصولك الآن. يرجى المحاولة مرة أخرى.', noInvoices: 'لا توجد فواتير حتى الآن.', noProviderConfigured: 'لا يوجد مزوّد دفع مهيأ. تواصل مع الدعم لتفعيل AmanPay أو PayPal.', providerUnavailable: 'مزوّد الدفع هذا غير مهيأ.', statusLabels: { TRIALING: 'تجريبي', ACTIVE: 'نشط', PAST_DUE: 'متأخر', CANCELLED: 'ملغى', CANCELED: 'ملغى', UNPAID: 'غير مدفوع', EXPIRED: 'منتهي', SUSPENDED: 'معلّق' } as Record, invoiceStatusLabels: { PAID: 'مدفوع', PENDING: 'قيد الانتظار', FAILED: 'فشل', REFUNDED: 'مسترد' } as Record, planFeatures: { STARTER: ['حتى 10 مركبات', 'مستخدم واحد', 'تحليلات أساسية', 'إدراج في السوق'], GROWTH: ['حتى 50 مركبة', '5 مستخدمين', 'تحليلات كاملة', 'إدراج ذو أولوية', 'تخصيص العلامة'], PRO: ['مركبات غير محدودة', 'مقاعد غير محدودة', 'تقارير متقدمة', 'وصول API', 'دعم مخصص'], } as Record, }, }[language] useEffect(() => { let cancelled = false setCanViewPage(null) setVerificationError(null) apiFetch<{ employee: EmployeeProfile }>('/auth/employee/me') .then(({ employee }) => { if (cancelled) return window.localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(employee)) const allowed = employee.role === 'OWNER' setCanViewPage(allowed) if (!allowed) router.replace('/') }) .catch((err: any) => { if (cancelled) return if (err?.statusCode === 401) { window.location.replace('/dashboard/sign-in?redirect=%2Fdashboard%2Fsubscription') return } if (err?.statusCode === 403) { setCanViewPage(false) return } setVerificationError(err?.message ?? copy.accessUnavailable) }) return () => { cancelled = true } }, [copy.accessUnavailable, router, verificationAttempt]) const fetchPlanData = useCallback(async () => { const [prices, features] = await Promise.all([ apiFetch>>>('/subscriptions/plans'), apiFetch('/subscriptions/features'), ]) if (prices && Object.keys(prices).length > 0) setPlanPrices(prices) if (features) setPlanFeaturesList(features) }, []) useEffect(() => { if (canViewPage !== true) return Promise.all([ apiFetch('/subscriptions/me'), apiFetch('/subscriptions/invoices'), apiFetch('/subscriptions/providers'), fetchPlanData(), ]) .then(([sub, inv, availability]) => { setProviderAvailability(availability) if (availability.amanpay) setProvider('AMANPAY') else if (availability.paypal) setProvider('PAYPAL') if (sub) { setSubscription(sub) setSelectedPlan(sub.plan) setBillingPeriod(sub.billingPeriod) // currency is always MAD } setInvoices(inv ?? []) }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) }, [canViewPage, fetchPlanData]) useEffect(() => { if (canViewPage !== true) return const handleFocus = () => { fetchPlanData().catch(() => {}) } const handleVisibility = () => { if (document.visibilityState === 'visible') fetchPlanData().catch(() => {}) } window.addEventListener('focus', handleFocus) document.addEventListener('visibilitychange', handleVisibility) return () => { window.removeEventListener('focus', handleFocus) document.removeEventListener('visibilitychange', handleVisibility) } }, [canViewPage, fetchPlanData]) if (verificationError) { return (

{copy.accessUnavailable}

{verificationError}

) } if (canViewPage === null) { return (
) } if (canViewPage !== true) { return (

{copy.accessDenied}

{copy.accessDeniedBody}

) } async function handleCheckout() { setPaying(true) setError(null) try { if (provider === 'AMANPAY' && !providerAvailability.amanpay) throw new Error(copy.providerUnavailable) if (provider === 'PAYPAL' && !providerAvailability.paypal) throw new Error(copy.providerUnavailable) const currentUrl = new URL(window.location.href) currentUrl.search = '' currentUrl.hash = '' const result = await apiFetch<{ checkoutUrl: string }>('/subscriptions/checkout', { method: 'POST', body: JSON.stringify({ plan: selectedPlan, billingPeriod, currency, provider, successUrl: `${currentUrl.toString()}?payment=success`, failureUrl: `${currentUrl.toString()}?payment=failed`, }), }) window.location.href = result.checkoutUrl } catch (err: any) { setError(err.message) setPaying(false) } } async function handleCancel() { setCancelling(true) setError(null) try { const sub = await apiFetch('/subscriptions/cancel', { method: 'POST' }) setSubscription(sub) } catch (err: any) { setError(err.message) } finally { setCancelling(false) } } async function handleResume() { setCancelling(true) setError(null) try { const sub = await apiFetch('/subscriptions/resume', { method: 'POST' }) setSubscription(sub) } catch (err: any) { setError(err.message) } finally { setCancelling(false) } } const planPrice = planPrices[selectedPlan]?.[billingPeriod]?.[currency] const daysLeft = subscription?.trialEndAt ? Math.ceil((new Date(subscription.trialEndAt).getTime() - Date.now()) / 86400000) : null return (

{copy.title}

{copy.subtitle}

{error && (
{error}
)} {/* Trial banner */} {subscription?.status === 'TRIALING' && daysLeft !== null && daysLeft > 0 && (

{copy.trial} — {daysLeft} days {copy.remaining}

)} {/* Current plan */} {subscription && (

{copy.currentPlan}

{subscription.plan}

{copy.statusLabels[subscription.status] ?? subscription.status}

{subscription.billingPeriod} · {subscription.currency} {subscription.currentPeriodEnd && ` · ${copy.renews} ${new Date(subscription.currentPeriodEnd).toLocaleDateString()}`}

{subscription.cancelAtPeriodEnd && (

{copy.cancelScheduled}{' '}

)}
{!subscription.cancelAtPeriodEnd && ( )}
)} {/* Plan selector + checkout */}
{!providerAvailability.amanpay && !providerAvailability.paypal ? (
{copy.noProviderConfigured}
) : null}

{subscription?.status === 'ACTIVE' ? copy.changePlan : copy.subscribe}

{copy.selectPlan}

{/* Billing period toggle */}
{(['MONTHLY', 'ANNUAL'] as BillingPeriod[]).map((p) => ( ))}
{/* Plan cards */}
{PLANS.map((plan) => { const price = planPrices[plan]?.[billingPeriod]?.[currency] const isActive = subscription?.plan === plan && subscription?.status === 'ACTIVE' const features = planFeaturesList.filter((f) => f.plan === plan) return ( ) })}
{/* Provider selector */}

{copy.paymentProvider}

{providerAvailability.amanpay ? ( ) : null} {providerAvailability.paypal ? ( ) : null}
{/* Checkout CTA */}

{copy.total}

{planPrice ? formatCurrency(planPrice, 'MAD') : '—'} /{billingPeriod === 'MONTHLY' ? copy.perMonth : copy.perYear}

{/* Invoice history */}

{copy.invoiceHistory}

{loading ? ( ) : invoices.length === 0 ? ( ) : invoices.map((inv) => ( ))}
{copy.date} {copy.provider} {copy.status} {copy.paid} {copy.amount}
{copy.loading}
{copy.noInvoices}
{new Date(inv.createdAt).toLocaleDateString()} {inv.paymentProvider} {copy.invoiceStatusLabels[inv.status] ?? inv.status} {inv.paidAt ? new Date(inv.paidAt).toLocaleDateString() : '—'} {formatCurrency(inv.amount, 'MAD')}
) }