fix subscription page
Build & Deploy / Build & Push Docker Image (push) Successful in 7m57s
Test / Type Check (all packages) (push) Successful in 4m27s
Build & Deploy / Deploy to VPS (push) Successful in 7s
Test / API Unit Tests (push) Failing after 3m2s
Test / Homepage Unit Tests (push) Successful in 4m3s
Test / Storefront Unit Tests (push) Successful in 3m32s
Test / Admin Unit Tests (push) Successful in 3m27s
Test / Dashboard Unit Tests (push) Successful in 3m3s
Test / API Integration Tests (push) Failing after 3m53s
Build & Deploy / Build & Push Docker Image (push) Successful in 7m57s
Test / Type Check (all packages) (push) Successful in 4m27s
Build & Deploy / Deploy to VPS (push) Successful in 7s
Test / API Unit Tests (push) Failing after 3m2s
Test / Homepage Unit Tests (push) Successful in 4m3s
Test / Storefront Unit Tests (push) Successful in 3m32s
Test / Admin Unit Tests (push) Successful in 3m27s
Test / Dashboard Unit Tests (push) Successful in 3m3s
Test / API Integration Tests (push) Failing after 3m53s
This commit is contained in:
@@ -51,7 +51,10 @@ const STATUS_BADGE: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
@@ -70,6 +73,8 @@ export default function SubscriptionPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [canViewPage, setCanViewPage] = useState<boolean | null>(null)
|
||||
const [verificationError, setVerificationError] = useState<string | null>(null)
|
||||
const [verificationAttempt, setVerificationAttempt] = useState(0)
|
||||
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan>('STARTER')
|
||||
const [billingPeriod, setBillingPeriod] = useState<BillingPeriod>('MONTHLY')
|
||||
@@ -96,7 +101,7 @@ export default function SubscriptionPage() {
|
||||
subscribe: 'Subscribe',
|
||||
selectPlan: 'Select a plan and payment provider to proceed.',
|
||||
monthly: 'Monthly',
|
||||
annual: 'Annual (save ~17%)',
|
||||
annual: 'Annual (save 20%)',
|
||||
active: 'Active',
|
||||
perMonthShort: 'mo',
|
||||
perYearShort: 'yr',
|
||||
@@ -113,10 +118,15 @@ export default function SubscriptionPage() {
|
||||
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', UNPAID: 'Unpaid' } as Record<string, string>,
|
||||
statusLabels: { TRIALING: 'Trialing', ACTIVE: 'Active', PAST_DUE: 'Past due', CANCELLED: 'Cancelled', CANCELED: 'Canceled', UNPAID: 'Unpaid', EXPIRED: 'Expired', SUSPENDED: 'Suspended' } as Record<string, string>,
|
||||
invoiceStatusLabels: { PAID: 'Paid', PENDING: 'Pending', FAILED: 'Failed', REFUNDED: 'Refunded' } as Record<string, string>,
|
||||
planFeatures: {
|
||||
STARTER: ['Up to 10 vehicles', '1 user seat', 'Basic analytics', 'Storefront listing'],
|
||||
@@ -139,7 +149,7 @@ export default function SubscriptionPage() {
|
||||
subscribe: 'S’abonner',
|
||||
selectPlan: 'Sélectionnez un plan et un prestataire de paiement.',
|
||||
monthly: 'Mensuel',
|
||||
annual: 'Annuel (économie ~17%)',
|
||||
annual: 'Annuel (économie 20%)',
|
||||
active: 'Actif',
|
||||
perMonthShort: 'mois',
|
||||
perYearShort: 'an',
|
||||
@@ -156,10 +166,15 @@ export default function SubscriptionPage() {
|
||||
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é', UNPAID: 'Impayé' } as Record<string, string>,
|
||||
statusLabels: { TRIALING: 'Essai', ACTIVE: 'Actif', PAST_DUE: 'En retard', CANCELLED: 'Annulé', CANCELED: 'Annulé', UNPAID: 'Impayé', EXPIRED: 'Expiré', SUSPENDED: 'Suspendu' } as Record<string, string>,
|
||||
invoiceStatusLabels: { PAID: 'Payé', PENDING: 'En attente', FAILED: 'Échec', REFUNDED: 'Remboursé' } as Record<string, string>,
|
||||
planFeatures: {
|
||||
STARTER: ['Jusqu’à 10 véhicules', '1 utilisateur', 'Analyses de base', 'Présence storefront'],
|
||||
@@ -182,7 +197,7 @@ export default function SubscriptionPage() {
|
||||
subscribe: 'اشتراك',
|
||||
selectPlan: 'اختر خطة ومزوّد دفع للمتابعة.',
|
||||
monthly: 'شهري',
|
||||
annual: 'سنوي (توفير ~17%)',
|
||||
annual: 'سنوي (توفير 20%)',
|
||||
active: 'نشط',
|
||||
perMonthShort: 'شهر',
|
||||
perYearShort: 'سنة',
|
||||
@@ -199,10 +214,15 @@ export default function SubscriptionPage() {
|
||||
paid: 'مدفوع',
|
||||
amount: 'المبلغ',
|
||||
loading: 'جارٍ التحميل…',
|
||||
verifying: 'جارٍ التحقق من الوصول…',
|
||||
accessDenied: 'تم رفض الوصول',
|
||||
accessDeniedBody: 'يمكن لمالكي الحساب فقط إدارة الاشتراكات.',
|
||||
retry: 'إعادة المحاولة',
|
||||
accessUnavailable: 'تعذر التحقق من وصولك الآن. يرجى المحاولة مرة أخرى.',
|
||||
noInvoices: 'لا توجد فواتير حتى الآن.',
|
||||
noProviderConfigured: 'لا يوجد مزوّد دفع مهيأ. تواصل مع الدعم لتفعيل AmanPay أو PayPal.',
|
||||
providerUnavailable: 'مزوّد الدفع هذا غير مهيأ.',
|
||||
statusLabels: { TRIALING: 'تجريبي', ACTIVE: 'نشط', PAST_DUE: 'متأخر', CANCELLED: 'ملغى', UNPAID: 'غير مدفوع' } as Record<string, string>,
|
||||
statusLabels: { TRIALING: 'تجريبي', ACTIVE: 'نشط', PAST_DUE: 'متأخر', CANCELLED: 'ملغى', CANCELED: 'ملغى', UNPAID: 'غير مدفوع', EXPIRED: 'منتهي', SUSPENDED: 'معلّق' } as Record<string, string>,
|
||||
invoiceStatusLabels: { PAID: 'مدفوع', PENDING: 'قيد الانتظار', FAILED: 'فشل', REFUNDED: 'مسترد' } as Record<string, string>,
|
||||
planFeatures: {
|
||||
STARTER: ['حتى 10 مركبات', 'مستخدم واحد', 'تحليلات أساسية', 'إدراج في السوق'],
|
||||
@@ -213,29 +233,34 @@ export default function SubscriptionPage() {
|
||||
}[language]
|
||||
|
||||
useEffect(() => {
|
||||
const cached = window.localStorage.getItem(EMPLOYEE_PROFILE_KEY)
|
||||
if (cached) {
|
||||
try {
|
||||
const profile = JSON.parse(cached) as EmployeeProfile
|
||||
const allowed = profile.role === 'OWNER'
|
||||
setCanViewPage(allowed)
|
||||
if (!allowed) router.replace('/')
|
||||
return
|
||||
} catch {}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
setCanViewPage(false)
|
||||
router.replace('/')
|
||||
.catch((err: any) => {
|
||||
if (cancelled) return
|
||||
if (err?.statusCode === 401) {
|
||||
router.replace('/sign-in?redirect=%2Fsubscription')
|
||||
return
|
||||
}
|
||||
if (err?.statusCode === 403) {
|
||||
setCanViewPage(false)
|
||||
return
|
||||
}
|
||||
setVerificationError(err?.message ?? copy.accessUnavailable)
|
||||
})
|
||||
}, [router])
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [copy.accessUnavailable, router, verificationAttempt])
|
||||
|
||||
const fetchPlanData = useCallback(async () => {
|
||||
const [prices, features] = await Promise.all([
|
||||
@@ -285,8 +310,41 @@ export default function SubscriptionPage() {
|
||||
}
|
||||
}, [canViewPage, fetchPlanData])
|
||||
|
||||
if (verificationError) {
|
||||
return (
|
||||
<div className="flex min-h-[40vh] items-center justify-center px-6">
|
||||
<div className="card max-w-md p-6 text-center">
|
||||
<h2 className="text-base font-semibold text-slate-900">{copy.accessUnavailable}</h2>
|
||||
<p className="mt-2 text-sm text-slate-500">{verificationError}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVerificationAttempt((value) => value + 1)}
|
||||
className="btn-primary mt-5"
|
||||
>
|
||||
{copy.retry}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (canViewPage === null) {
|
||||
return (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-orange-500 border-t-transparent" aria-label={copy.verifying} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (canViewPage !== true) {
|
||||
return null
|
||||
return (
|
||||
<div className="flex min-h-[40vh] items-center justify-center px-6">
|
||||
<div className="card max-w-md p-6 text-center">
|
||||
<h2 className="text-base font-semibold text-slate-900">{copy.accessDenied}</h2>
|
||||
<p className="mt-2 text-sm text-slate-500">{copy.accessDeniedBody}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function handleCheckout() {
|
||||
|
||||
Reference in New Issue
Block a user