Files
smoothschedule/frontend/src/components/PaymentSettingsSection.tsx
poduck 2e111364a2 Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:43:20 -05:00

221 lines
7.5 KiB
TypeScript

/**
* Payment Settings Section Component
* Unified payment configuration UI that shows the appropriate setup
* based on the business tier (API keys for Free, Connect for Paid)
*/
import React from 'react';
import {
CreditCard,
CheckCircle,
AlertCircle,
Loader2,
FlaskConical,
Zap,
} from 'lucide-react';
import { Business } from '../types';
import { usePaymentConfig } from '../hooks/usePayments';
import StripeApiKeysForm from './StripeApiKeysForm';
import ConnectOnboardingEmbed from './ConnectOnboardingEmbed';
interface PaymentSettingsSectionProps {
business: Business;
}
type PaymentModeType = 'direct_api' | 'connect' | 'none';
const PaymentSettingsSection: React.FC<PaymentSettingsSectionProps> = ({ business }) => {
const { data: config, isLoading, error, refetch } = usePaymentConfig();
if (isLoading) {
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center gap-3">
<Loader2 className="animate-spin text-gray-400" size={24} />
<span className="text-gray-600">Loading payment configuration...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle size={24} />
<span>Failed to load payment configuration</span>
</div>
<button
onClick={() => refetch()}
className="mt-3 px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
>
Retry
</button>
</div>
);
}
const paymentMode = (config?.payment_mode || 'none') as PaymentModeType;
const canAcceptPayments = config?.can_accept_payments || false;
const tier = config?.tier || business.plan || 'Free';
const isFreeTier = tier === 'Free';
// Determine Stripe environment (test vs live) from API keys
const getStripeEnvironment = (): 'test' | 'live' | null => {
const maskedKey = config?.api_keys?.publishable_key_masked;
if (!maskedKey) return null;
if (maskedKey.startsWith('pk_test_')) return 'test';
if (maskedKey.startsWith('pk_live_')) return 'live';
return null;
};
const stripeEnvironment = getStripeEnvironment();
// Status badge component
const StatusBadge = () => {
if (canAcceptPayments) {
return (
<span className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-full">
<CheckCircle size={12} />
Ready
</span>
);
}
return (
<span className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded-full">
<AlertCircle size={12} />
Setup Required
</span>
);
};
// Mode description
const getModeDescription = () => {
if (isFreeTier) {
return 'Free tier businesses use their own Stripe API keys for payment processing. No platform fees apply.';
}
return `${tier} tier businesses use Stripe Connect for payment processing with platform-managed payments.`;
};
return (
<div className="bg-white rounded-lg shadow">
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<CreditCard className="text-purple-600" size={24} />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900">Payment Configuration</h2>
<p className="text-sm text-gray-500">{getModeDescription()}</p>
</div>
</div>
<StatusBadge />
</div>
</div>
{/* Test/Live Mode Banner */}
{stripeEnvironment && config?.api_keys?.status === 'active' && (
<div
className={`px-6 py-3 flex items-center gap-3 ${
stripeEnvironment === 'test'
? 'bg-amber-50 border-b border-amber-200'
: 'bg-green-50 border-b border-green-200'
}`}
>
{stripeEnvironment === 'test' ? (
<>
<div className="p-2 bg-amber-100 rounded-full">
<FlaskConical className="text-amber-600" size={20} />
</div>
<div className="flex-1">
<p className="font-semibold text-amber-800">Test Mode</p>
<p className="text-sm text-amber-700">
Payments are simulated. No real money will be charged.
</p>
</div>
<a
href="https://dashboard.stripe.com/test/apikeys"
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-amber-700 hover:text-amber-800 underline"
>
Get Live Keys
</a>
</>
) : (
<>
<div className="p-2 bg-green-100 rounded-full">
<Zap className="text-green-600" size={20} />
</div>
<div className="flex-1">
<p className="font-semibold text-green-800">Live Mode</p>
<p className="text-sm text-green-700">
Payments are real. Customers will be charged.
</p>
</div>
</>
)}
</div>
)}
{/* Content */}
<div className="p-6">
{/* Tier info banner */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-gray-600">Current Plan:</span>
<span className={`ml-2 px-2 py-0.5 text-xs font-semibold rounded-full ${
tier === 'Enterprise' ? 'bg-purple-100 text-purple-800' :
tier === 'Business' ? 'bg-blue-100 text-blue-800' :
tier === 'Professional' ? 'bg-green-100 text-green-800' :
'bg-gray-100 text-gray-800'
}`}>
{tier}
</span>
</div>
<div className="text-sm text-gray-600">
Payment Mode:{' '}
<span className="font-medium text-gray-900">
{paymentMode === 'direct_api' ? 'Direct API Keys' :
paymentMode === 'connect' ? 'Stripe Connect' :
'Not Configured'}
</span>
</div>
</div>
</div>
{/* Tier-specific content */}
{isFreeTier ? (
<StripeApiKeysForm
apiKeys={config?.api_keys || null}
onSuccess={() => refetch()}
/>
) : (
<ConnectOnboardingEmbed
connectAccount={config?.connect_account || null}
tier={tier}
onComplete={() => refetch()}
/>
)}
{/* Upgrade notice for free tier with deprecated keys */}
{isFreeTier && config?.api_keys?.status === 'deprecated' && (
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-800 mb-1">
Upgraded to a Paid Plan?
</h4>
<p className="text-sm text-blue-700">
If you've recently upgraded, your API keys have been deprecated.
Please contact support to complete your Stripe Connect setup.
</p>
</div>
)}
</div>
</div>
);
};
export default PaymentSettingsSection;