'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { createPortal } from 'react-dom' import { useRouter, useSearchParams } from 'next/navigation' import { setSessionAction } from '@/app/actions/auth' import { logger } from '@/lib/utils/logger' import { useAuth } from '@/lib/auth/context' import { deleteOrganization, switchContext, getOrganizationMembers, getInvitations, sendInvitation, revokeInvitation, updateOrganization, getOrganization, OrganizationMember, OrganizationInvitation, Organization } from '@/lib/api/organization' import { getSubscription, createPortalSession, getOrders, cancelSubscription, resumeSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Order } from '@/lib/api/billing' 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' import { getAuthErrorMessage } from '@ciphera-net/ui' import { formatDate, formatDateTime, formatDateLong } from '@/lib/utils/formatDate' import { motion, AnimatePresence } from 'framer-motion' import { AlertTriangleIcon, PlusIcon, BoxIcon, UserIcon, CheckIcon, XIcon, Captcha, BookOpenIcon, ExternalLinkIcon, LayoutDashboardIcon, Spinner, } from '@ciphera-net/ui' import { MembersListSkeleton, InvoicesListSkeleton, AuditLogSkeleton, SettingsFormSkeleton, SkeletonCard } from '@/components/skeletons' // * Bell icon for notifications tab function BellIcon({ className }: { className?: string }) { return ( ) } import { Button, Input } from '@ciphera-net/ui' export default function OrganizationSettings() { const { user } = useAuth() const router = useRouter() const searchParams = useSearchParams() // Initialize from URL, default to 'general' const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing' | 'notifications' | 'audit'>(() => { const tab = searchParams.get('tab') return (tab === 'billing' || tab === 'members' || tab === 'notifications' || tab === 'audit') ? tab : 'general' }) // Sync URL with state without triggering navigation/reload const handleTabChange = (tab: 'general' | 'members' | 'billing' | 'notifications' | 'audit') => { setActiveTab(tab) const url = new URL(window.location.href) url.searchParams.set('tab', tab) window.history.replaceState({}, '', url) } const [showDeletePrompt, setShowDeletePrompt] = useState(false) const [deleteConfirm, setDeleteConfirm] = useState('') const [isDeleting, setIsDeleting] = useState(false) const [isLoadingDeleteData, setIsLoadingDeleteData] = useState(false) // Members State const [members, setMembers] = useState([]) const [invitations, setInvitations] = useState([]) const [isLoadingMembers, setIsLoadingMembers] = useState(true) // Billing State const [subscription, setSubscription] = useState(null) const [isLoadingSubscription, setIsLoadingSubscription] = useState(false) const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false) const [cancelLoadingAction, setCancelLoadingAction] = useState<'period_end' | 'immediate' | null>(null) 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 [isChangingPlan, setIsChangingPlan] = useState(false) const [orders, setOrders] = useState([]) const [isLoadingInvoices, setIsLoadingInvoices] = useState(false) // Invite State const [inviteEmail, setInviteEmail] = useState('') const [inviteRole, setInviteRole] = useState('member') const [isInviting, setIsInviting] = useState(false) // Captcha State const [captchaId, setCaptchaId] = useState('') const [captchaSolution, setCaptchaSolution] = useState('') const [captchaToken, setCaptchaToken] = useState('') // Org Update State const [orgDetails, setOrgDetails] = useState(null) const [isEditing, setIsEditing] = useState(false) const [orgName, setOrgName] = useState('') const [orgSlug, setOrgSlug] = useState('') const [isSaving, setIsSaving] = useState(false) // Audit log State const [auditEntries, setAuditEntries] = useState([]) const [auditTotal, setAuditTotal] = useState(0) const [isLoadingAudit, setIsLoadingAudit] = useState(false) const [auditPage, setAuditPage] = useState(0) const [auditFetchTrigger, setAuditFetchTrigger] = useState(0) const auditPageSize = 20 const [auditActionFilter, setAuditActionFilter] = useState('') const [auditLogIdFilter, setAuditLogIdFilter] = useState('') const [auditStartDate, setAuditStartDate] = useState('') const [auditEndDate, setAuditEndDate] = useState('') // Notification settings state const [notificationSettings, setNotificationSettings] = useState>({}) const [notificationCategories, setNotificationCategories] = useState<{ id: string; label: string; description: string }[]>([]) const [isLoadingNotificationSettings, setIsLoadingNotificationSettings] = useState(false) const [isSavingNotificationSettings, setIsSavingNotificationSettings] = useState(false) // Refs for filters to keep loadAudit stable and avoid rapid re-renders const filtersRef = useRef({ action: auditActionFilter, logId: auditLogIdFilter, startDate: auditStartDate, endDate: auditEndDate }) // Update refs when state changes (no useEffect needed) filtersRef.current = { action: auditActionFilter, logId: auditLogIdFilter, startDate: auditStartDate, endDate: auditEndDate } const getOrgIdFromToken = () => { return user?.org_id || null } const currentOrgId = getOrgIdFromToken() const loadMembers = useCallback(async () => { if (!currentOrgId) { setIsLoadingMembers(false) return } setIsLoadingMembers(true) try { const [membersData, invitesData, orgData] = await Promise.all([ getOrganizationMembers(currentOrgId), getInvitations(currentOrgId), getOrganization(currentOrgId) ]) setMembers(membersData) setInvitations(invitesData) setOrgDetails(orgData) setOrgName(orgData.name) setOrgSlug(orgData.slug) } catch (error) { logger.error('Failed to load data:', error) // toast.error('Failed to load members') } finally { setIsLoadingMembers(false) } }, [currentOrgId]) const loadSubscription = useCallback(async () => { if (!currentOrgId) return setIsLoadingSubscription(true) try { const sub = await getSubscription() setSubscription(sub) } catch (error) { logger.error('Failed to load subscription:', error) // toast.error('Failed to load subscription details') } finally { setIsLoadingSubscription(false) } }, [currentOrgId]) const loadOrders = useCallback(async () => { if (!currentOrgId) return setIsLoadingInvoices(true) try { const ords = await getOrders() setOrders(ords) } catch (error) { logger.error('Failed to load orders:', error) } finally { setIsLoadingInvoices(false) } }, [currentOrgId]) useEffect(() => { if (currentOrgId) { loadMembers() } else { setIsLoadingMembers(false) } }, [currentOrgId, loadMembers]) // Removed useEffect that syncs searchParams to activeTab to prevent flickering // The initial state is already set from searchParams, and handleTabChange updates the URL manually /* useEffect(() => { const tab = searchParams.get('tab') const validTab = (tab === 'billing' || tab === 'members' || tab === 'audit') ? tab : 'general' if (validTab !== activeTab) { setActiveTab(validTab) } }, [searchParams, activeTab]) */ useEffect(() => { if (activeTab === 'billing' && currentOrgId) { loadSubscription() loadOrders() } }, [activeTab, currentOrgId, loadSubscription, loadOrders]) const loadAudit = useCallback(async () => { if (!currentOrgId) return setIsLoadingAudit(true) try { const params: GetAuditLogParams = { limit: auditPageSize, offset: auditPage * auditPageSize, } if (filtersRef.current.action) params.action = filtersRef.current.action if (filtersRef.current.logId) params.log_id = filtersRef.current.logId if (filtersRef.current.startDate) params.start_date = filtersRef.current.startDate if (filtersRef.current.endDate) params.end_date = filtersRef.current.endDate const { entries, total } = await getAuditLog(params) setAuditEntries(Array.isArray(entries) ? entries : []) setAuditTotal(typeof total === 'number' ? total : 0) } catch (error) { logger.error('Failed to load audit log', error) toast.error(getAuthErrorMessage(error as Error) || 'Failed to load audit log entries') } finally { setIsLoadingAudit(false) } }, [currentOrgId, auditPage]) // Debounced filter change handler useEffect(() => { if (activeTab !== 'audit') return const timer = setTimeout(() => { setAuditPage(0) // Reset page on filter change setAuditFetchTrigger(prev => prev + 1) // Trigger fetch }, 500) return () => clearTimeout(timer) }, [auditActionFilter, auditLogIdFilter, auditStartDate, auditEndDate]) useEffect(() => { if (activeTab === 'audit' && currentOrgId) { loadAudit() } }, [activeTab, currentOrgId, loadAudit, auditFetchTrigger]) const loadNotificationSettings = useCallback(async () => { if (!currentOrgId) return setIsLoadingNotificationSettings(true) try { const res = await getNotificationSettings() setNotificationSettings(res.settings || {}) setNotificationCategories(res.categories || []) } catch (error) { logger.error('Failed to load notification settings', error) toast.error(getAuthErrorMessage(error as Error) || 'Failed to load notification settings') } finally { setIsLoadingNotificationSettings(false) } }, [currentOrgId]) useEffect(() => { if (activeTab === 'notifications' && currentOrgId && user?.role !== 'member') { loadNotificationSettings() } }, [activeTab, currentOrgId, loadNotificationSettings, user?.role]) // * Redirect members away from Notifications tab (owners/admins only) useEffect(() => { if (activeTab === 'notifications' && user?.role === 'member') { handleTabChange('general') } }, [activeTab, user?.role, handleTabChange]) const hasActiveSubscription = subscription?.subscription_status === 'active' || subscription?.subscription_status === 'trialing' useEffect(() => { if (!showChangePlanModal || !hasActiveSubscription) { return } }, [showChangePlanModal, hasActiveSubscription, changePlanId, changePlanTierIndex, changePlanYearly]) // If no org ID, we are in personal organization context, so don't show org settings if (!currentOrgId) { return (

You are in your personal context. Switch to an Organization to manage its settings.

) } const handleManageSubscription = async () => { setIsRedirectingToPortal(true) try { const { url } = await createPortalSession() window.location.href = url } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to open billing portal') setIsRedirectingToPortal(false) } } const handleCancelSubscription = async (atPeriodEnd: boolean) => { setCancelLoadingAction(atPeriodEnd ? 'period_end' : 'immediate') try { await cancelSubscription({ at_period_end: atPeriodEnd }) toast.success(atPeriodEnd ? 'Subscription will cancel at the end of the billing period.' : 'Subscription canceled.') setShowCancelPrompt(false) loadSubscription() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to cancel subscription') } finally { setCancelLoadingAction(null) } } const handleResumeSubscription = async () => { setIsResuming(true) try { await resumeSubscription() toast.success('Subscription will continue. Cancellation has been undone.') loadSubscription() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to resume subscription') } finally { setIsResuming(false) } } 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 { setChangePlanTierIndex(2) } setChangePlanYearly(subscription?.billing_interval === 'year') setShowChangePlanModal(true) } const handleChangePlanSubmit = async () => { const interval = changePlanYearly ? 'year' : 'month' const limit = getLimitForTierIndex(changePlanTierIndex) setIsChangingPlan(true) try { if (hasActiveSubscription) { 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: changePlanId, interval, limit }) if (url) window.location.href = url else throw new Error('No checkout URL') } } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to update plan') } finally { setIsChangingPlan(false) } } const handleDelete = async () => { if (deleteConfirm !== 'DELETE') return setIsDeleting(true) try { await deleteOrganization(currentOrgId) toast.success('Organization deleted successfully') // * Clear sticky session localStorage.removeItem('active_org_id') // * Switch to personal context explicitly try { const { access_token } = await switchContext(null) await setSessionAction(access_token) sessionStorage.setItem('pulse_switching_org', 'true') window.location.href = '/' } catch (switchErr) { logger.error('Failed to switch to personal context after delete:', switchErr) sessionStorage.setItem('pulse_switching_org', 'true') window.location.href = '/' } } catch (err: unknown) { logger.error(err) toast.error(getAuthErrorMessage(err) || (err instanceof Error ? err.message : '') || 'Failed to delete organization') setIsDeleting(false) } } const handleSendInvite = async (e: React.FormEvent) => { e.preventDefault() if (!inviteEmail) return if (!captchaToken) { toast.error('Please complete the security check') return } setIsInviting(true) try { await sendInvitation(currentOrgId, inviteEmail, inviteRole, { captcha_id: captchaId, captcha_solution: captchaSolution, captcha_token: captchaToken }) toast.success(`Invitation sent to ${inviteEmail}`) setInviteEmail('') // Reset captcha setCaptchaId('') setCaptchaSolution('') setCaptchaToken('') loadMembers() // Refresh list } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to send invitation') } finally { setIsInviting(false) } } const handleRevokeInvite = async (inviteId: string) => { try { await revokeInvitation(currentOrgId, inviteId) toast.success('Invitation revoked') loadMembers() // Refresh list } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to revoke invitation') } } const handleUpdateOrg = async (e: React.FormEvent) => { e.preventDefault() if (!currentOrgId) return setIsSaving(true) try { await updateOrganization(currentOrgId, orgName, orgSlug) toast.success('Organization updated successfully') setIsEditing(false) loadMembers() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to save organization settings') } finally { setIsSaving(false) } } // Helper to find current org name (from members list if available, or just fallback) // Ideally we'd have a full org object, but we have ID. // We can find the current user's membership entry which has org name. const currentOrgName = members.find(m => m.user_id === user?.id)?.organization_name || 'Organization' // handleTabChange is defined above return (

Organization Settings

Manage your organization workspace and members.

{/* Sidebar Navigation */} {/* Content Area - no min-w-0 so it uses full available width and table doesn't get cramped */}
{activeTab === 'general' && (

General Information

Basic details about your organization.

) => setOrgName(e.target.value)} required minLength={2} maxLength={50} disabled={!isEditing} className={`bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-400' : ''}`} />
pulse.ciphera.net/ ) => setOrgSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))} required minLength={3} maxLength={30} disabled={!isEditing} className={`rounded-l-none bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-400' : ''}`} />

Changing the slug will change your organization's URL.

{!isEditing ? ( ) : ( <> )}

Danger Zone

Irreversible actions for this organization.

Delete Organization

Permanently delete this organization and all its data.

)} {activeTab === 'members' && (
{/* Invite Section */}

Organization Members

Manage who has access to this organization.

Invite New Member

) => setInviteEmail(e.target.value)} required className="bg-white dark:bg-neutral-900" />
{ setCaptchaId(id) setCaptchaSolution(solution) setCaptchaToken(token || '') }} apiUrl={process.env.NEXT_PUBLIC_CAPTCHA_API_URL} action="org-settings" />
{/* Members List */}

Active Members

{isLoadingMembers ? ( ) : members.length === 0 ? (
No members found.
) : ( members.map((member) => (
{member.user_email?.[0].toUpperCase() || '?'}
{member.user_email || 'Unknown User'}
Joined {formatDate(new Date(member.joined_at))}
{member.role}
)) )}
{/* Pending Invitations */} {invitations.length > 0 && (

Pending Invitations

{invitations.map((invite) => (
{invite.email}
Invited as {invite.role} • Expires {formatDate(new Date(invite.expires_at))}
))}
)}
)} {activeTab === 'billing' && (

Billing & Subscription

Manage your plan, usage, and payment details.

{isLoadingSubscription ? (
) : !subscription ? (

Could not load subscription details.

) : (
{/* Trial notice */} {subscription.subscription_status === 'trialing' && (

Your free trial ends on{' '} {(() => { const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null return d && !Number.isNaN(d.getTime()) ? formatDateLong(d) : '—' })()}

After the trial you'll be charged automatically unless you cancel before then.

)} {/* Past due notice */} {subscription.subscription_status === 'past_due' && (

Payment past due

We couldn't charge your payment method. Please update your billing info to avoid service interruption.

)} {/* Cancel-at-period-end notice */} {subscription.cancel_at_period_end && (

Your subscription will end on{' '} {(() => { const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null return d && !Number.isNaN(d.getTime()) ? formatDateLong(d) : '—' })()}

You keep full access until then. Your data is retained for 30 days after. Use "Change plan" to resubscribe.

)} {/* Plan & Usage card */}
{/* Plan header */}
{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan {subscription.subscription_status === 'trialing' ? 'Trial' : subscription.subscription_status === 'past_due' ? 'Past Due' : (subscription.subscription_status || 'Free')} {subscription.billing_interval && ( Billed {subscription.billing_interval}ly )}
{(subscription.business_name || subscription.tax_id) && (
{subscription.business_name && (
Billing for: {subscription.business_name}
)} {subscription.tax_id && ( Tax ID: {subscription.tax_id.value} ({subscription.tax_id.type}) )}
)} {/* Usage stats */}
Sites
{typeof subscription.sites_count === 'number' ? (() => { const limit = getSitesLimitForPlan(subscription.plan_id) return limit != null ? `${subscription.sites_count} / ${limit}` : `${subscription.sites_count}` })() : '—'}
Pageviews
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' ? `${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)}%` }} />
)}
{subscription.subscription_status === 'trialing' ? 'Trial ends' : (subscription.cancel_at_period_end ? 'Access until' : 'Renews')}
{(() => { const ts = subscription.current_period_end const d = ts ? new Date(ts) : null return d && !Number.isNaN(d.getTime()) && d.getTime() !== 0 ? formatDate(d) : '—' })()}
Limit
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / mo` : 'Unlimited'}
{/* Quick actions */}
{subscription.has_payment_method && ( )} {subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && !subscription.cancel_at_period_end && ( )}
{/* Order History */}

Recent orders

{isLoadingInvoices ? ( ) : orders.length === 0 ? (
No orders found.
) : ( <> {orders.map((order) => (
{(order.total_amount / 100).toLocaleString('en-US', { style: 'currency', currency: order.currency.toUpperCase() })} {formatDate(new Date(order.created_at))}
{order.status}
))} ) }
)}
)} {activeTab === 'notifications' && (

Notification Settings

Choose which notification types you want to receive. These apply to the notification center for owners and admins.

{isLoadingNotificationSettings ? ( ) : (

Notification categories

{notificationCategories.map((cat) => (

{cat.label}

{cat.description}

))}
)}
)} {activeTab === 'audit' && (

Audit log

Who did what and when for this organization.

{/* Advanced Filters */}
setAuditLogIdFilter(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all" />
setAuditActionFilter(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all" />
setAuditStartDate(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all" />
setAuditEndDate(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all" />
{/* Table */}
{isLoadingAudit ? ( ) : (auditEntries ?? []).length === 0 ? (
No audit events found.
) : (
{(auditEntries ?? []).map((entry) => ( ))}
Log ID Time Actor Action Resource
{entry.id} {formatDateTime(new Date(entry.occurred_at))} {entry.actor_email || entry.actor_id || 'System'} {entry.action} {entry.resource_type}
)} {/* Pagination */} {auditTotal > auditPageSize && (
{auditPage * auditPageSize + 1}–{Math.min((auditPage + 1) * auditPageSize, auditTotal)} of {auditTotal}
)}
)}
{/* Delete Confirmation Modal — portal to body so backdrop-blur covers the fixed header */} {typeof document !== 'undefined' && createPortal( {showDeletePrompt && (

Delete {orgName || 'Organization'}?

This action is irreversible. The following will be permanently deleted:

{typeof subscription?.sites_count === 'number' && subscription.sites_count > 0 && (
{subscription.sites_count} {subscription.sites_count === 1 ? 'site' : 'sites'} and all analytics data
)} {members.length > 1 && (
{members.length - 1} {members.length - 1 === 1 ? 'member' : 'members'} will be removed
)} {subscription?.plan_id && subscription.plan_id !== 'free' && (
Active subscription will be cancelled
)}
All notifications and settings
setDeleteConfirm(e.target.value)} autoComplete="off" className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400" placeholder="DELETE" />
)}
, document.body )} {/* Cancel subscription confirmation modal */} {showCancelPrompt && (

Cancel subscription?

You can cancel at the end of your billing period (you keep access until then) or cancel immediately. Your data is retained for 30 days after access ends.

)}
{/* Change plan modal */} {showChangePlanModal && (

Change plan

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 && (

Your plan will be updated. Any prorations will be reflected on your next invoice.

)}
)}
) }