feat: Stripe subscriptions, tier-based permissions, dark mode, and UX improvements

- Fix Stripe SDK v14 compatibility (bracket notation for subscription items)
- Fix subscription period dates from subscription items instead of subscription object
- Add tier-based permissions (can_accept_payments, etc.) on tenant signup
- Add stripe_customer_id field to Tenant model
- Add clickable logo on login page (navigates to /)
- Add database setup message during signup wizard
- Add dark mode support for payment settings and Connect onboarding
- Add subscription management endpoints (cancel, reactivate)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-02 20:50:18 -05:00
parent 08b51d1a5f
commit ef58e9fc94
12 changed files with 1337 additions and 167 deletions

View File

@@ -5,6 +5,7 @@
*/
import React from 'react';
import { useNavigate } from 'react-router-dom';
import {
CreditCard,
CheckCircle,
@@ -12,6 +13,8 @@ import {
Loader2,
FlaskConical,
Zap,
ArrowUpRight,
Sparkles,
} from 'lucide-react';
import { Business } from '../types';
import { usePaymentConfig } from '../hooks/usePayments';
@@ -25,6 +28,7 @@ interface PaymentSettingsSectionProps {
type PaymentModeType = 'direct_api' | 'connect' | 'none';
const PaymentSettingsSection: React.FC<PaymentSettingsSectionProps> = ({ business }) => {
const navigate = useNavigate();
const { data: config, isLoading, error, refetch } = usePaymentConfig();
if (isLoading) {
@@ -56,6 +60,8 @@ const PaymentSettingsSection: React.FC<PaymentSettingsSectionProps> = ({ busines
}
const paymentMode = (config?.payment_mode || 'none') as PaymentModeType;
const tierAllowsPayments = config?.tier_allows_payments || false;
const stripeConfigured = config?.stripe_configured || false;
const canAcceptPayments = config?.can_accept_payments || false;
const tier = config?.tier || business.plan || 'Free';
const isFreeTier = tier === 'Free';
@@ -72,16 +78,24 @@ const PaymentSettingsSection: React.FC<PaymentSettingsSectionProps> = ({ busines
// Status badge component
const StatusBadge = () => {
if (!tierAllowsPayments) {
return (
<span className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full">
<AlertCircle size={12} />
Upgrade Required
</span>
);
}
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">
<span className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 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">
<span className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 rounded-full">
<AlertCircle size={12} />
Setup Required
</span>
@@ -97,17 +111,17 @@ const PaymentSettingsSection: React.FC<PaymentSettingsSectionProps> = ({ busines
};
return (
<div className="bg-white rounded-lg shadow">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<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 className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<CreditCard className="text-purple-600 dark:text-purple-400" 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>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Payment Configuration</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">{getModeDescription()}</p>
</div>
</div>
<StatusBadge />
@@ -162,22 +176,22 @@ const PaymentSettingsSection: React.FC<PaymentSettingsSectionProps> = ({ busines
{/* Content */}
<div className="p-6">
{/* Tier info banner */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-gray-600">Current Plan:</span>
<span className="text-sm text-gray-600 dark:text-gray-400">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 === 'Enterprise' ? 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300' :
tier === 'Business' ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300' :
tier === 'Professional' ? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300' :
'bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200'
}`}>
{tier}
</span>
</div>
<div className="text-sm text-gray-600">
<div className="text-sm text-gray-600 dark:text-gray-400">
Payment Mode:{' '}
<span className="font-medium text-gray-900">
<span className="font-medium text-gray-900 dark:text-white">
{paymentMode === 'direct_api' ? 'Direct API Keys' :
paymentMode === 'connect' ? 'Stripe Connect' :
'Not Configured'}
@@ -186,31 +200,86 @@ const PaymentSettingsSection: React.FC<PaymentSettingsSectionProps> = ({ busines
</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>
{/* Upgrade prompt when tier doesn't allow payments */}
{!tierAllowsPayments ? (
<div className="bg-gradient-to-br from-purple-50 to-indigo-50 border border-purple-200 rounded-xl p-6">
<div className="flex items-start gap-4">
<div className="p-3 bg-purple-100 rounded-xl">
<Sparkles className="text-purple-600" size={28} />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Unlock Online Payments
</h3>
<p className="text-gray-600 mb-4">
Your current plan doesn't include online payment processing. Upgrade your subscription
or add the Online Payments add-on to start accepting payments from your customers.
</p>
<ul className="space-y-2 mb-6 text-sm text-gray-600">
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
Accept credit cards, debit cards, and digital wallets
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
Automatic invoicing and payment reminders
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
Secure PCI-compliant payment processing
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
Detailed transaction history and analytics
</li>
</ul>
<div className="flex flex-wrap gap-3">
<button
onClick={() => navigate('/settings/billing')}
className="flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors"
>
<ArrowUpRight size={16} />
View Upgrade Options
</button>
<a
href="mailto:support@smoothschedule.com?subject=Online Payments Add-on"
className="flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-purple-700 bg-purple-100 rounded-lg hover:bg-purple-200 transition-colors"
>
Contact Sales
</a>
</div>
</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>