- 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>
291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
/**
|
|
* Embedded Stripe Connect Onboarding Component
|
|
*
|
|
* Uses Stripe's Connect embedded components to provide a seamless
|
|
* onboarding experience without redirecting users away from the app.
|
|
*/
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
import {
|
|
ConnectComponentsProvider,
|
|
ConnectAccountOnboarding,
|
|
} from '@stripe/react-connect-js';
|
|
import { loadConnectAndInitialize } from '@stripe/connect-js';
|
|
import type { StripeConnectInstance } from '@stripe/connect-js';
|
|
import {
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Loader2,
|
|
CreditCard,
|
|
Wallet,
|
|
Building2,
|
|
} from 'lucide-react';
|
|
import { createAccountSession, refreshConnectStatus, ConnectAccountInfo } from '../api/payments';
|
|
|
|
interface ConnectOnboardingEmbedProps {
|
|
connectAccount: ConnectAccountInfo | null;
|
|
tier: string;
|
|
onComplete?: () => void;
|
|
onError?: (error: string) => void;
|
|
}
|
|
|
|
type LoadingState = 'idle' | 'loading' | 'ready' | 'error' | 'complete';
|
|
|
|
const ConnectOnboardingEmbed: React.FC<ConnectOnboardingEmbedProps> = ({
|
|
connectAccount,
|
|
tier,
|
|
onComplete,
|
|
onError,
|
|
}) => {
|
|
const [stripeConnectInstance, setStripeConnectInstance] = useState<StripeConnectInstance | null>(null);
|
|
const [loadingState, setLoadingState] = useState<LoadingState>('idle');
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
|
|
const isActive = connectAccount?.status === 'active' && connectAccount?.charges_enabled;
|
|
|
|
// Initialize Stripe Connect
|
|
const initializeStripeConnect = useCallback(async () => {
|
|
if (loadingState === 'loading' || loadingState === 'ready') return;
|
|
|
|
setLoadingState('loading');
|
|
setErrorMessage(null);
|
|
|
|
try {
|
|
// Fetch account session from our backend
|
|
const response = await createAccountSession();
|
|
const { client_secret, publishable_key } = response.data;
|
|
|
|
// Initialize the Connect instance
|
|
const instance = await loadConnectAndInitialize({
|
|
publishableKey: publishable_key,
|
|
fetchClientSecret: async () => client_secret,
|
|
appearance: {
|
|
overlays: 'drawer',
|
|
variables: {
|
|
colorPrimary: '#635BFF',
|
|
colorBackground: '#ffffff',
|
|
colorText: '#1a1a1a',
|
|
colorDanger: '#df1b41',
|
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
fontSizeBase: '14px',
|
|
spacingUnit: '4px',
|
|
borderRadius: '8px',
|
|
},
|
|
},
|
|
});
|
|
|
|
setStripeConnectInstance(instance);
|
|
setLoadingState('ready');
|
|
} catch (err: any) {
|
|
console.error('Failed to initialize Stripe Connect:', err);
|
|
const message = err.response?.data?.error || err.message || 'Failed to initialize payment setup';
|
|
setErrorMessage(message);
|
|
setLoadingState('error');
|
|
onError?.(message);
|
|
}
|
|
}, [loadingState, onError]);
|
|
|
|
// Handle onboarding completion
|
|
const handleOnboardingExit = useCallback(async () => {
|
|
// Refresh status from Stripe to sync the local database
|
|
try {
|
|
await refreshConnectStatus();
|
|
} catch (err) {
|
|
console.error('Failed to refresh Connect status:', err);
|
|
}
|
|
setLoadingState('complete');
|
|
onComplete?.();
|
|
}, [onComplete]);
|
|
|
|
// Handle errors from the Connect component
|
|
const handleLoadError = useCallback((loadError: { error: { message?: string }; elementTagName: string }) => {
|
|
console.error('Connect component load error:', loadError);
|
|
const message = loadError.error.message || 'Failed to load payment component';
|
|
setErrorMessage(message);
|
|
setLoadingState('error');
|
|
onError?.(message);
|
|
}, [onError]);
|
|
|
|
// Account type display
|
|
const getAccountTypeLabel = () => {
|
|
switch (connectAccount?.account_type) {
|
|
case 'standard':
|
|
return 'Standard Connect';
|
|
case 'express':
|
|
return 'Express Connect';
|
|
case 'custom':
|
|
return 'Custom Connect';
|
|
default:
|
|
return 'Connect';
|
|
}
|
|
};
|
|
|
|
// If account is already active, show status
|
|
if (isActive) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<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 dark:text-green-400 shrink-0 mt-0.5" size={20} />
|
|
<div className="flex-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 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 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 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 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 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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Completion state
|
|
if (loadingState === 'complete') {
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
|
|
// Error state
|
|
if (loadingState === 'error') {
|
|
return (
|
|
<div className="space-y-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 dark:text-red-400 shrink-0 mt-0.5" size={20} />
|
|
<div className="flex-1">
|
|
<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>
|
|
<button
|
|
onClick={() => {
|
|
setLoadingState('idle');
|
|
setErrorMessage(null);
|
|
}}
|
|
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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Idle state - show start button
|
|
if (loadingState === 'idle') {
|
|
return (
|
|
<div className="space-y-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 dark:text-blue-400 shrink-0 mt-0.5" size={20} />
|
|
<div className="flex-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 dark:text-blue-400">
|
|
<li className="flex items-center gap-2">
|
|
<CheckCircle size={14} />
|
|
Secure payment processing
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<CheckCircle size={14} />
|
|
Automatic payouts to your bank account
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<CheckCircle size={14} />
|
|
PCI compliance handled for you
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={initializeStripeConnect}
|
|
className="w-full flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium text-white bg-[#635BFF] rounded-lg hover:bg-[#5851ea] transition-colors"
|
|
>
|
|
<CreditCard size={18} />
|
|
Start Payment Setup
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Loading state
|
|
if (loadingState === 'loading') {
|
|
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 dark:text-gray-400">Initializing payment setup...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Ready state - show embedded onboarding
|
|
if (loadingState === 'ready' && stripeConnectInstance) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<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 dark:border-gray-600 rounded-lg overflow-hidden bg-white dark:bg-gray-800 p-4">
|
|
<ConnectComponentsProvider connectInstance={stripeConnectInstance}>
|
|
<ConnectAccountOnboarding
|
|
onExit={handleOnboardingExit}
|
|
onLoadError={handleLoadError}
|
|
/>
|
|
</ConnectComponentsProvider>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
export default ConnectOnboardingEmbed;
|