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:
@@ -124,41 +124,41 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
if (isActive) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="text-green-600 shrink-0 mt-0.5" size={20} />
|
||||
<CheckCircle className="text-green-600 dark:text-green-400 shrink-0 mt-0.5" size={20} />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-green-800">Stripe Connected</h4>
|
||||
<p className="text-sm text-green-700 mt-1">
|
||||
<h4 className="font-medium text-green-800 dark:text-green-300">Stripe Connected</h4>
|
||||
<p className="text-sm text-green-700 dark:text-green-400 mt-1">
|
||||
Your Stripe account is connected and ready to accept payments.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">Account Details</h4>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Account Details</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Account Type:</span>
|
||||
<span className="text-gray-900">{getAccountTypeLabel()}</span>
|
||||
<span className="text-gray-600 dark:text-gray-400">Account Type:</span>
|
||||
<span className="text-gray-900 dark:text-white">{getAccountTypeLabel()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Status:</span>
|
||||
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-800">
|
||||
<span className="text-gray-600 dark:text-gray-400">Status:</span>
|
||||
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300">
|
||||
{connectAccount.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Charges:</span>
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<span className="text-gray-600 dark:text-gray-400">Charges:</span>
|
||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||
<CreditCard size={14} />
|
||||
Enabled
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Payouts:</span>
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<span className="text-gray-600 dark:text-gray-400">Payouts:</span>
|
||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||
<Wallet size={14} />
|
||||
{connectAccount.payouts_enabled ? 'Enabled' : 'Pending'}
|
||||
</span>
|
||||
@@ -172,10 +172,10 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
// Completion state
|
||||
if (loadingState === 'complete') {
|
||||
return (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 text-center">
|
||||
<CheckCircle className="mx-auto text-green-600 mb-3" size={48} />
|
||||
<h4 className="font-medium text-green-800 text-lg">Onboarding Complete!</h4>
|
||||
<p className="text-sm text-green-700 mt-2">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6 text-center">
|
||||
<CheckCircle className="mx-auto text-green-600 dark:text-green-400 mb-3" size={48} />
|
||||
<h4 className="font-medium text-green-800 dark:text-green-300 text-lg">Onboarding Complete!</h4>
|
||||
<p className="text-sm text-green-700 dark:text-green-400 mt-2">
|
||||
Your Stripe account has been set up. You can now accept payments.
|
||||
</p>
|
||||
</div>
|
||||
@@ -186,12 +186,12 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
if (loadingState === 'error') {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="text-red-600 shrink-0 mt-0.5" size={20} />
|
||||
<AlertCircle className="text-red-600 dark:text-red-400 shrink-0 mt-0.5" size={20} />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-red-800">Setup Failed</h4>
|
||||
<p className="text-sm text-red-700 mt-1">{errorMessage}</p>
|
||||
<h4 className="font-medium text-red-800 dark:text-red-300">Setup Failed</h4>
|
||||
<p className="text-sm text-red-700 dark:text-red-400 mt-1">{errorMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,7 +200,7 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
setLoadingState('idle');
|
||||
setErrorMessage(null);
|
||||
}}
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
@@ -212,16 +212,16 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
if (loadingState === 'idle') {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Building2 className="text-blue-600 shrink-0 mt-0.5" size={20} />
|
||||
<Building2 className="text-blue-600 dark:text-blue-400 shrink-0 mt-0.5" size={20} />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-blue-800">Set Up Payments</h4>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
<h4 className="font-medium text-blue-800 dark:text-blue-300">Set Up Payments</h4>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-400 mt-1">
|
||||
As a {tier} tier business, you'll use Stripe Connect to accept payments.
|
||||
Complete the onboarding process to start accepting payments from your customers.
|
||||
</p>
|
||||
<ul className="mt-3 space-y-1 text-sm text-blue-700">
|
||||
<ul className="mt-3 space-y-1 text-sm text-blue-700 dark:text-blue-400">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={14} />
|
||||
Secure payment processing
|
||||
@@ -255,7 +255,7 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<Loader2 className="animate-spin text-[#635BFF] mb-4" size={40} />
|
||||
<p className="text-gray-600">Initializing payment setup...</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">Initializing payment setup...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -264,15 +264,15 @@ const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
||||
if (loadingState === 'ready' && stripeConnectInstance) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Complete Your Account Setup</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Complete Your Account Setup</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Fill out the information below to finish setting up your payment account.
|
||||
Your information is securely handled by Stripe.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white p-4">
|
||||
<div className="border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden bg-white dark:bg-gray-800 p-4">
|
||||
<ConnectComponentsProvider connectInstance={stripeConnectInstance}>
|
||||
<ConnectAccountOnboarding
|
||||
onExit={handleOnboardingExit}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user