147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
import { mdiCreditCardOutline } from '@mdi/js'
|
|
import axios from 'axios'
|
|
import React, { useEffect, useState } from 'react'
|
|
import BaseButton from './BaseButton'
|
|
import CardBox from './CardBox'
|
|
import { useAppSelector } from '../stores/hooks'
|
|
import { isInternalAdmin } from '../helpers/portalRoles'
|
|
|
|
type LimitKey = 'monthlyReviewRequests' | 'businesses' | 'teamMembers' | 'paymentConnectors'
|
|
|
|
type SubscriptionLimitStatus = {
|
|
subscription: {
|
|
planId: string
|
|
planName: string
|
|
effectiveStatus: string
|
|
isActive: boolean
|
|
}
|
|
usage: Record<LimitKey, number>
|
|
limits: Record<LimitKey, number>
|
|
}
|
|
|
|
type Props = {
|
|
limitKey: LimitKey
|
|
actionLabel: string
|
|
label?: string
|
|
className?: string
|
|
nearLimitPercent?: number
|
|
}
|
|
|
|
const defaultLabels: Record<LimitKey, string> = {
|
|
monthlyReviewRequests: 'review requests this month',
|
|
businesses: 'business profiles',
|
|
teamMembers: 'team members',
|
|
paymentConnectors: 'connected payment providers',
|
|
}
|
|
|
|
function formatNumber(value: number) {
|
|
return value.toLocaleString()
|
|
}
|
|
|
|
export default function SubscriptionLimitGate({
|
|
limitKey,
|
|
actionLabel,
|
|
label,
|
|
className = 'mb-6',
|
|
nearLimitPercent = 80,
|
|
}: Props) {
|
|
const { currentUser } = useAppSelector((state) => state.auth)
|
|
const [status, setStatus] = useState<SubscriptionLimitStatus | null>(null)
|
|
const [error, setError] = useState('')
|
|
|
|
useEffect(() => {
|
|
let isMounted = true
|
|
|
|
const loadStatus = async () => {
|
|
try {
|
|
const response = await axios.get('/subscription/me')
|
|
|
|
if (isMounted) {
|
|
setStatus(response.data)
|
|
setError('')
|
|
}
|
|
} catch (requestError) {
|
|
console.error('Failed to load subscription limit status:', requestError)
|
|
|
|
if (isMounted) {
|
|
setError('Could not check plan limits right now. The backend will still enforce them when you submit.')
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!currentUser || isInternalAdmin(currentUser)) {
|
|
setStatus(null)
|
|
setError('')
|
|
return () => {
|
|
isMounted = false
|
|
}
|
|
}
|
|
|
|
loadStatus()
|
|
|
|
return () => {
|
|
isMounted = false
|
|
}
|
|
}, [currentUser])
|
|
|
|
if (error) {
|
|
return (
|
|
<CardBox className={`${className} border-0 bg-amber-50 text-amber-950 ring-1 ring-amber-200 dark:bg-amber-950 dark:text-amber-50 dark:ring-amber-800`}>
|
|
<p className='text-sm font-black uppercase tracking-[0.25em]'>Plan check unavailable</p>
|
|
<p className='mt-2 text-sm leading-6'>{error}</p>
|
|
</CardBox>
|
|
)
|
|
}
|
|
|
|
if (!status) {
|
|
return null
|
|
}
|
|
|
|
const used = Number(status.usage[limitKey]) || 0
|
|
const limit = Number(status.limits[limitKey]) || 0
|
|
const limitLabel = label || (limit === 1 && limitKey === 'businesses'
|
|
? 'business profile'
|
|
: defaultLabels[limitKey])
|
|
const percent = limit > 0 ? Math.round((used / limit) * 100) : 0
|
|
const isInactive = !status.subscription.isActive
|
|
const isBlocked = isInactive || (limit > 0 && used >= limit)
|
|
const isNearLimit = !isBlocked && percent >= nearLimitPercent
|
|
|
|
if (!isBlocked && !isNearLimit) {
|
|
return null
|
|
}
|
|
|
|
const cardClass = isBlocked
|
|
? 'border-0 bg-rose-50 text-rose-950 ring-1 ring-rose-200 dark:bg-rose-950 dark:text-rose-50 dark:ring-rose-800'
|
|
: 'border-0 bg-amber-50 text-amber-950 ring-1 ring-amber-200 dark:bg-amber-950 dark:text-amber-50 dark:ring-amber-800'
|
|
const buttonLabel = status.subscription.planId === 'starter' ? 'Upgrade to Pro' : 'Manage plan'
|
|
|
|
return (
|
|
<CardBox className={`${className} ${cardClass}`}>
|
|
<div className='flex flex-col gap-4 md:flex-row md:items-center md:justify-between'>
|
|
<div>
|
|
<p className='text-sm font-black uppercase tracking-[0.25em]'>
|
|
{isBlocked ? 'Plan limit reached' : 'Plan limit almost reached'}
|
|
</p>
|
|
<h3 className='mt-2 text-xl font-black'>
|
|
{actionLabel} {isBlocked ? 'may be blocked' : 'is getting close to the limit'}
|
|
</h3>
|
|
<p className='mt-2 text-sm leading-6'>
|
|
{isInactive
|
|
? `Your ${status.subscription.planName} plan is ${status.subscription.effectiveStatus}. Reactivate or choose a plan before continuing.`
|
|
: `${status.subscription.planName} includes ${formatNumber(limit)} ${limitLabel}. This account is using ${formatNumber(used)}.`}
|
|
{' '}Existing data stays available.
|
|
</p>
|
|
</div>
|
|
<BaseButton
|
|
href='/subscription'
|
|
icon={mdiCreditCardOutline}
|
|
label={buttonLabel}
|
|
color={isBlocked ? 'danger' : 'warning'}
|
|
className='self-start md:self-center'
|
|
/>
|
|
</div>
|
|
</CardBox>
|
|
)
|
|
}
|