/** * 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, RefreshCw, Layers, Plus, Pencil, Trash2, X, DollarSign, Check, Lock, Users, ExternalLink, } from 'lucide-react'; import { usePlatformSettings, useUpdateStripeKeys, useValidateStripeKeys, useSubscriptionPlans, useCreateSubscriptionPlan, useUpdateSubscriptionPlan, useDeleteSubscriptionPlan, useSyncPlansWithStripe, SubscriptionPlan, SubscriptionPlanCreate, } from '../../hooks/usePlatformSettings'; import { usePlatformOAuthSettings, useUpdatePlatformOAuthSettings, } from '../../hooks/usePlatformOAuth'; type TabType = 'stripe' | 'tiers' | 'oauth'; const PlatformSettings: React.FC = () => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('stripe'); const tabs: { id: TabType; label: string; icon: React.ElementType }[] = [ { 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 === 'stripe' && } {activeTab === 'tiers' && } {activeTab === 'oauth' && }
); }; const StripeSettingsTab: React.FC = () => { 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 (
Failed to load settings
); } return (
{/* Status Card */}

Stripe Configuration Status

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

API Keys

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

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

Validation

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

{settings?.stripe_account_id && (

Account ID: {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

Secret Key {settings?.stripe_secret_key_masked || 'Not configured'}
Publishable Key {settings?.stripe_publishable_key_masked || 'Not configured'}
Webhook Secret {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 { data: plans, isLoading, error } = useSubscriptionPlans(); const createPlanMutation = useCreateSubscriptionPlan(); const updatePlanMutation = useUpdateSubscriptionPlan(); const deletePlanMutation = useDeleteSubscriptionPlan(); const syncMutation = useSyncPlansWithStripe(); const [showModal, setShowModal] = useState(false); const [editingPlan, setEditingPlan] = 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 }); } else { await createPlanMutation.mutateAsync(data); } setShowModal(false); setEditingPlan(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 */}

Base Tiers

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

Add-ons

{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} /> )}
); }; 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.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 || [], transaction_fee_percent: plan?.transaction_fee_percent ? parseFloat(plan.transaction_fee_percent) : 0, transaction_fee_fixed: plan?.transaction_fee_fixed ? parseFloat(plan.transaction_fee_fixed) : 0, is_active: plan?.is_active ?? true, is_public: plan?.is_public ?? 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 handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSave(formData); }; return (

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

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" />