/** * Platform Settings Page * Allows superusers to configure platform-wide settings including Stripe credentials and tiers */ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Settings, CreditCard, Shield, CheckCircle, AlertCircle, Loader2, Eye, EyeOff, Layers, Plus, Pencil, Trash2, X, DollarSign, Check, Lock, Users, ExternalLink, Mail, RefreshCw, } from 'lucide-react'; import { usePlatformSettings, useUpdateStripeKeys, useValidateStripeKeys, useUpdateGeneralSettings, useSubscriptionPlans, useCreateSubscriptionPlan, useUpdateSubscriptionPlan, useDeleteSubscriptionPlan, useSyncPlansWithStripe, useSyncPlanToTenants, SubscriptionPlan, SubscriptionPlanCreate, } from '../../hooks/usePlatformSettings'; import { usePlatformOAuthSettings, useUpdatePlatformOAuthSettings, } from '../../hooks/usePlatformOAuth'; import { Link } from 'react-router-dom'; import FeaturesPermissionsEditor, { getPermissionKey, PERMISSION_DEFINITIONS } from '../../components/platform/FeaturesPermissionsEditor'; type TabType = 'general' | 'stripe' | 'tiers' | 'oauth'; const PlatformSettings: React.FC = () => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('general'); const tabs: { id: TabType; label: string; icon: React.ElementType }[] = [ { id: 'general', label: t('platform.settings.general', 'General'), icon: Settings }, { id: 'stripe', label: 'Stripe', icon: CreditCard }, { id: 'tiers', label: t('platform.settings.tiersPricing'), icon: Layers }, { id: 'oauth', label: t('platform.settings.oauthProviders'), icon: Users }, ]; return (

{t('platform.settings.title')}

{t('platform.settings.description')}

{/* Tabs */}
{/* Tab Content */} {activeTab === 'general' && } {activeTab === 'stripe' && } {activeTab === 'tiers' && } {activeTab === 'oauth' && }
); }; const GeneralSettingsTab: React.FC = () => { const { t } = useTranslation(); const { data: settings, isLoading } = usePlatformSettings(); const updateGeneralSettings = useUpdateGeneralSettings(); const [emailCheckInterval, setEmailCheckInterval] = useState(5); const [hasChanges, setHasChanges] = useState(false); // Sync local state with settings when loaded React.useEffect(() => { if (settings?.email_check_interval_minutes) { setEmailCheckInterval(settings.email_check_interval_minutes); } }, [settings]); const handleIntervalChange = (value: number) => { setEmailCheckInterval(value); setHasChanges(value !== settings?.email_check_interval_minutes); }; const handleSaveInterval = async () => { await updateGeneralSettings.mutateAsync({ email_check_interval_minutes: emailCheckInterval }); setHasChanges(false); }; if (isLoading) { return (
); } return (
{/* Email Settings Card */}

{t('platform.settings.emailAddresses', 'Platform Email Addresses')}

Platform email addresses are now managed on a dedicated page with direct integration to the mail server.

Manage Email Addresses
{/* Email Check Interval */}

Email Polling Settings

{hasChanges && ( )}

This controls how often the system checks for incoming emails to create support tickets.

{updateGeneralSettings.isSuccess && (

Email polling interval updated

)}
{/* Platform Info */}

{t('platform.settings.platformInfo', 'Platform Information')}

{t('platform.settings.mailServer')}

mail.talova.net

{t('platform.settings.emailDomain')}

smoothschedule.com

); }; const StripeSettingsTab: React.FC = () => { const { t } = useTranslation(); const { data: settings, isLoading, error } = usePlatformSettings(); const updateKeysMutation = useUpdateStripeKeys(); const validateKeysMutation = useValidateStripeKeys(); const [secretKey, setSecretKey] = useState(''); const [publishableKey, setPublishableKey] = useState(''); const [webhookSecret, setWebhookSecret] = useState(''); const [showSecretKey, setShowSecretKey] = useState(false); const [showWebhookSecret, setShowWebhookSecret] = useState(false); const handleSaveKeys = async () => { const keys: Record = {}; if (secretKey) keys.stripe_secret_key = secretKey; if (publishableKey) keys.stripe_publishable_key = publishableKey; if (webhookSecret) keys.stripe_webhook_secret = webhookSecret; if (Object.keys(keys).length === 0) return; await updateKeysMutation.mutateAsync(keys); setSecretKey(''); setPublishableKey(''); setWebhookSecret(''); }; const handleValidate = async () => { await validateKeysMutation.mutateAsync(); }; if (isLoading) { return (
); } if (error) { return (
{t('platform.settings.failedToLoadSettings')}
); } return (
{/* Status Card */}

{t('platform.settings.stripeConfigStatus')}

{settings?.has_stripe_keys ? ( ) : ( )}

API Keys

{settings?.has_stripe_keys ? 'Configured' : 'Not configured'}

{settings?.stripe_keys_validated_at ? ( ) : ( )}

{t('platform.settings.validation')}

{settings?.stripe_keys_validated_at ? `Validated ${new Date(settings.stripe_keys_validated_at).toLocaleDateString()}` : 'Not validated'}

{settings?.stripe_account_id && (

{t('platform.settings.accountId')}: {settings.stripe_account_id} {settings.stripe_account_name && ( ({settings.stripe_account_name}) )}

)} {settings?.stripe_validation_error && (

Error: {settings.stripe_validation_error}

)}
{/* Current Keys Card */}

Current API Keys

{t('platform.settings.secretKey')} {settings?.stripe_secret_key_masked || 'Not configured'}
{t('platform.settings.publishableKey')} {settings?.stripe_publishable_key_masked || 'Not configured'}
{t('platform.settings.webhookSecret')} {settings?.stripe_webhook_secret_masked || 'Not configured'}
{settings?.has_stripe_keys && (
)}
{/* Update Keys Form - Only show if keys are NOT from environment variables */} {settings?.stripe_keys_from_env ? (

Stripe Keys Configured via Environment Variables

Your Stripe API keys are configured through environment variables (.env file). To update them, modify your environment configuration and restart the server.

Environment variables take priority over database-stored keys for security.

) : (

Update API Keys

Enter new keys to update. Leave fields empty to keep existing values.

setSecretKey(e.target.value)} placeholder="sk_live_... or sk_test_..." className="w-full px-4 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
setPublishableKey(e.target.value)} placeholder="pk_live_... or pk_test_..." className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
setWebhookSecret(e.target.value)} placeholder="whsec_..." className="w-full px-4 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
{updateKeysMutation.isError && (

Failed to update keys. Please check the format and try again.

)} {updateKeysMutation.isSuccess && (

Keys updated successfully

)}
)}
); }; const TiersSettingsTab: React.FC = () => { const { t } = useTranslation(); const { data: plans, isLoading, error } = useSubscriptionPlans(); const createPlanMutation = useCreateSubscriptionPlan(); const updatePlanMutation = useUpdateSubscriptionPlan(); const deletePlanMutation = useDeleteSubscriptionPlan(); const syncMutation = useSyncPlansWithStripe(); const syncTenantsMutation = useSyncPlanToTenants(); const [showModal, setShowModal] = useState(false); const [editingPlan, setEditingPlan] = useState(null); const [showSyncConfirmModal, setShowSyncConfirmModal] = useState(false); const [savedPlanForSync, setSavedPlanForSync] = useState(null); const handleCreatePlan = () => { setEditingPlan(null); setShowModal(true); }; const handleEditPlan = (plan: SubscriptionPlan) => { setEditingPlan(plan); setShowModal(true); }; const handleDeletePlan = async (plan: SubscriptionPlan) => { if (confirm(`Are you sure you want to deactivate the "${plan.name}" plan?`)) { await deletePlanMutation.mutateAsync(plan.id); } }; const handleSavePlan = async (data: SubscriptionPlanCreate) => { if (editingPlan) { await updatePlanMutation.mutateAsync({ id: editingPlan.id, ...data }); // After updating an existing plan, ask if they want to sync to tenants setSavedPlanForSync(editingPlan); setShowSyncConfirmModal(true); } else { await createPlanMutation.mutateAsync(data); } setShowModal(false); setEditingPlan(null); }; const handleSyncConfirm = async () => { if (savedPlanForSync) { await syncTenantsMutation.mutateAsync(savedPlanForSync.id); } setShowSyncConfirmModal(false); setSavedPlanForSync(null); }; const handleSyncCancel = () => { setShowSyncConfirmModal(false); setSavedPlanForSync(null); }; if (isLoading) { return (
); } if (error) { return (
Failed to load subscription plans
); } const basePlans = plans?.filter((p) => p.plan_type === 'base') || []; const addonPlans = plans?.filter((p) => p.plan_type === 'addon') || []; return (
{/* Header */}

Subscription Plans

Configure pricing tiers and add-ons for businesses

{/* Base Plans */}

{t('platform.settings.baseTiers')}

{basePlans.length === 0 ? (
No base tiers configured. Click "Add Plan" to create one.
) : ( basePlans.map((plan) => ( handleEditPlan(plan)} onDelete={() => handleDeletePlan(plan)} /> )) )}
{/* Add-on Plans */}

{t('platform.settings.addOns')}

{addonPlans.length === 0 ? (
No add-ons configured.
) : ( addonPlans.map((plan) => ( handleEditPlan(plan)} onDelete={() => handleDeletePlan(plan)} /> )) )}
{/* Plan Modal */} {showModal && ( { setShowModal(false); setEditingPlan(null); }} isLoading={createPlanMutation.isPending || updatePlanMutation.isPending} /> )} {/* Sync Confirmation Modal */} {showSyncConfirmModal && savedPlanForSync && (

Update All Tenants?

Do you want to sync the updated settings to all tenants currently on the "{savedPlanForSync.name}" plan?

This will update permissions and limits for all businesses on this tier.

)}
); }; interface PlanRowProps { plan: SubscriptionPlan; onEdit: () => void; onDelete: () => void; } const PlanRow: React.FC = ({ plan, onEdit, onDelete }) => { return (

{plan.name}

{!plan.is_active && ( Inactive )} {!plan.is_public && plan.is_active && ( Hidden )} {plan.is_most_popular && ( Popular )} {!plan.show_price && ( Price Hidden )} {plan.business_tier && ( {plan.business_tier} )}

{plan.description}

{plan.price_monthly && ( {parseFloat(plan.price_monthly).toFixed(2)}/mo )} {plan.features.length > 0 && ( {plan.features.length} features )} {parseFloat(plan.transaction_fee_percent) > 0 && ( {plan.transaction_fee_percent}% fee )}
); }; interface PlanModalProps { plan: SubscriptionPlan | null; onSave: (data: SubscriptionPlanCreate) => void; onClose: () => void; isLoading: boolean; } const PlanModal: React.FC = ({ plan, onSave, onClose, isLoading }) => { const [formData, setFormData] = useState({ name: plan?.name || '', description: plan?.description || '', plan_type: plan?.plan_type || 'base', price_monthly: plan?.price_monthly ? parseFloat(plan.price_monthly) : undefined, price_yearly: plan?.price_yearly ? parseFloat(plan.price_yearly) : undefined, business_tier: plan?.business_tier || '', features: plan?.features || [], limits: plan?.limits || { max_users: 5, max_resources: 10, max_appointments: 100, max_automated_tasks: 5, }, permissions: plan?.permissions || { can_accept_payments: false, sms_reminders: false, advanced_reporting: false, priority_support: false, can_use_custom_domain: false, can_use_plugins: false, can_use_tasks: false, can_create_plugins: false, can_white_label: false, can_api_access: false, can_use_masked_phone_numbers: false, }, // Default transaction fees: Stripe charges 2.9% + $0.30, so we need to charge more // Recommended: FREE 5%+50¢, STARTER 4%+40¢, PROFESSIONAL 3.5%+35¢, ENTERPRISE 3%+30¢ transaction_fee_percent: plan?.transaction_fee_percent ? parseFloat(plan.transaction_fee_percent) : 4.0, // Default 4% for new plans transaction_fee_fixed: plan?.transaction_fee_fixed ? parseFloat(plan.transaction_fee_fixed) : 40, // Default 40 cents for new plans // Communication pricing sms_enabled: plan?.sms_enabled ?? false, sms_price_per_message_cents: plan?.sms_price_per_message_cents ?? 3, masked_calling_enabled: plan?.masked_calling_enabled ?? false, masked_calling_price_per_minute_cents: plan?.masked_calling_price_per_minute_cents ?? 5, proxy_number_enabled: plan?.proxy_number_enabled ?? false, proxy_number_monthly_fee_cents: plan?.proxy_number_monthly_fee_cents ?? 200, // Contracts feature contracts_enabled: plan?.contracts_enabled ?? false, // Default credit settings default_auto_reload_enabled: plan?.default_auto_reload_enabled ?? false, default_auto_reload_threshold_cents: plan?.default_auto_reload_threshold_cents ?? 1000, default_auto_reload_amount_cents: plan?.default_auto_reload_amount_cents ?? 2500, is_active: plan?.is_active ?? true, is_public: plan?.is_public ?? true, is_most_popular: plan?.is_most_popular ?? false, show_price: plan?.show_price ?? true, create_stripe_product: false, stripe_product_id: plan?.stripe_product_id || '', stripe_price_id: plan?.stripe_price_id || '', }); const [newFeature, setNewFeature] = useState(''); const handleAddFeature = () => { if (newFeature.trim()) { setFormData((prev) => ({ ...prev, features: [...(prev.features || []), newFeature.trim()], })); setNewFeature(''); } }; const handleRemoveFeature = (index: number) => { setFormData((prev) => ({ ...prev, features: prev.features?.filter((_, i) => i !== index) || [], })); }; const handleLimitChange = (key: string, value: string) => { setFormData((prev) => ({ ...prev, limits: { ...prev.limits, [key]: parseInt(value) || 0, }, })); }; const handlePermissionChange = (key: string, value: boolean) => { setFormData((prev) => ({ ...prev, permissions: { ...prev.permissions, [key]: value, }, })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSave(formData); }; return (

{plan ? 'Edit Plan' : 'Create Plan'}

{/* Basic Info */}

Basic Information

setFormData((prev) => ({ ...prev, name: e.target.value }))} required className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />