Enterprise
For high volume sites and custom needs
diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx
index 6474be3..4e5aaa7 100644
--- a/components/settings/OrganizationSettings.tsx
+++ b/components/settings/OrganizationSettings.tsx
@@ -17,7 +17,7 @@ import {
Organization
} from '@/lib/api/organization'
import { getSubscription, createPortalSession, getInvoices, cancelSubscription, resumeSubscription, changePlan, previewInvoice, createCheckoutSession, SubscriptionDetails, Invoice, PreviewInvoiceResult } from '@/lib/api/billing'
-import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans'
+import { TRAFFIC_TIERS, PLAN_ID_SOLO, PLAN_ID_TEAM, PLAN_ID_BUSINESS, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans'
import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit'
import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings'
import { toast } from '@ciphera-net/ui'
@@ -85,6 +85,7 @@ export default function OrganizationSettings() {
const [showCancelPrompt, setShowCancelPrompt] = useState(false)
const [isResuming, setIsResuming] = useState(false)
const [showChangePlanModal, setShowChangePlanModal] = useState(false)
+ const [changePlanId, setChangePlanId] = useState
(PLAN_ID_SOLO)
const [changePlanTierIndex, setChangePlanTierIndex] = useState(2)
const [changePlanYearly, setChangePlanYearly] = useState(false)
const [invoicePreview, setInvoicePreview] = useState(null)
@@ -345,6 +346,12 @@ export default function OrganizationSettings() {
}
const openChangePlanModal = () => {
+ const currentPlan = subscription?.plan_id
+ if (currentPlan === PLAN_ID_TEAM || currentPlan === PLAN_ID_BUSINESS) {
+ setChangePlanId(currentPlan)
+ } else {
+ setChangePlanId(PLAN_ID_SOLO)
+ }
if (subscription?.pageview_limit != null && subscription.pageview_limit > 0) {
setChangePlanTierIndex(getTierIndexForLimit(subscription.pageview_limit))
} else {
@@ -367,11 +374,11 @@ export default function OrganizationSettings() {
setInvoicePreview(null)
const interval = changePlanYearly ? 'year' : 'month'
const limit = getLimitForTierIndex(changePlanTierIndex)
- previewInvoice({ plan_id: PLAN_ID_SOLO, interval, limit })
+ previewInvoice({ plan_id: changePlanId, interval, limit })
.then((res) => { if (!cancelled) setInvoicePreview(res ?? null) })
.finally(() => { if (!cancelled) setIsLoadingPreview(false) })
return () => { cancelled = true }
- }, [showChangePlanModal, hasActiveSubscription, changePlanTierIndex, changePlanYearly])
+ }, [showChangePlanModal, hasActiveSubscription, changePlanId, changePlanTierIndex, changePlanYearly])
const handleChangePlanSubmit = async () => {
const interval = changePlanYearly ? 'year' : 'month'
@@ -379,12 +386,12 @@ export default function OrganizationSettings() {
setIsChangingPlan(true)
try {
if (hasActiveSubscription) {
- await changePlan({ plan_id: PLAN_ID_SOLO, interval, limit })
+ await changePlan({ plan_id: changePlanId, interval, limit })
toast.success('Plan updated. Changes may take a moment to reflect.')
setShowChangePlanModal(false)
loadSubscription()
} else {
- const { url } = await createCheckoutSession({ plan_id: PLAN_ID_SOLO, interval, limit })
+ const { url } = await createCheckoutSession({ plan_id: changePlanId, interval, limit })
if (url) window.location.href = url
else throw new Error('No checkout URL')
}
@@ -844,6 +851,29 @@ export default function OrganizationSettings() {
)}
+ {/* Past due notice */}
+ {subscription.subscription_status === 'past_due' && (
+
+ )}
+
{/* Cancel-at-period-end notice */}
{subscription.cancel_at_period_end && (
@@ -886,9 +916,11 @@ export default function OrganizationSettings() {
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: subscription.subscription_status === 'trialing'
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'
+ : subscription.subscription_status === 'past_due'
+ ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300'
}`}>
- {subscription.subscription_status === 'trialing' ? 'Trial' : (subscription.subscription_status || 'Free')}
+ {subscription.subscription_status === 'trialing' ? 'Trial' : subscription.subscription_status === 'past_due' ? 'Past Due' : (subscription.subscription_status || 'Free')}
{subscription.billing_interval && (
@@ -918,7 +950,7 @@ export default function OrganizationSettings() {
)}
{/* Usage stats */}
-
+
Sites
@@ -937,6 +969,22 @@ export default function OrganizationSettings() {
? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}`
: '—'}
+ {subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && (
+
+
= 1
+ ? 'bg-red-500'
+ : subscription.pageview_usage / subscription.pageview_limit >= 0.9
+ ? 'bg-red-400'
+ : subscription.pageview_usage / subscription.pageview_limit >= 0.8
+ ? 'bg-amber-400'
+ : 'bg-green-500'
+ }`}
+ style={{ width: `${Math.min(100, (subscription.pageview_usage / subscription.pageview_limit) * 100)}%` }}
+ />
+
+ )}
@@ -975,7 +1023,7 @@ export default function OrganizationSettings() {
type="button"
onClick={handleManageSubscription}
disabled={isRedirectingToPortal}
- className="inline-flex items-center gap-1.5 text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors disabled:opacity-50"
+ className="inline-flex items-center gap-1.5 text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:rounded"
>
Payment method & invoices
@@ -985,7 +1033,7 @@ export default function OrganizationSettings() {
@@ -994,7 +1042,7 @@ export default function OrganizationSettings() {
{/* Invoice History */}
-
Recent invoices
+
Recent invoices
{isLoadingInvoices ? (
@@ -1028,14 +1076,14 @@ export default function OrganizationSettings() {
{invoice.invoice_pdf && (
+ className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange" title="Download PDF">
Download PDF
)}
{invoice.hosted_invoice_url && (
+ className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange" title="View invoice">
View invoice
@@ -1411,8 +1459,9 @@ export default function OrganizationSettings() {
@@ -1421,6 +1470,41 @@ export default function OrganizationSettings() {
Choose your pageview limit and billing interval. {hasActiveSubscription ? 'Your next invoice will reflect prorations.' : 'You’ll start a new subscription.'}
+
+
+
+ {([
+ { id: PLAN_ID_SOLO, name: 'Solo', sites: '1 site' },
+ { id: PLAN_ID_TEAM, name: 'Team', sites: 'Up to 5 sites' },
+ { id: PLAN_ID_BUSINESS, name: 'Business', sites: 'Up to 10 sites' },
+ ] as const).map((plan) => {
+ const isCurrentPlan = subscription?.plan_id === plan.id
+ const isSelected = changePlanId === plan.id
+ return (
+
+ )
+ })}
+
+
{hasActiveSubscription && (
-