Files
smoothschedule/frontend/src/components/ConnectOnboarding.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

270 lines
9.5 KiB
TypeScript

/**
* Stripe Connect Onboarding Component
* For paid-tier businesses to connect their Stripe account via Connect
*/
import React, { useState } from 'react';
import {
ExternalLink,
CheckCircle,
AlertCircle,
Loader2,
RefreshCw,
CreditCard,
Wallet,
} from 'lucide-react';
import { ConnectAccountInfo } from '../api/payments';
import { useConnectOnboarding, useRefreshConnectLink } from '../hooks/usePayments';
interface ConnectOnboardingProps {
connectAccount: ConnectAccountInfo | null;
tier: string;
onSuccess?: () => void;
}
const ConnectOnboarding: React.FC<ConnectOnboardingProps> = ({
connectAccount,
tier,
onSuccess,
}) => {
const [error, setError] = useState<string | null>(null);
const onboardingMutation = useConnectOnboarding();
const refreshLinkMutation = useRefreshConnectLink();
const isActive = connectAccount?.status === 'active' && connectAccount?.charges_enabled;
const isOnboarding = connectAccount?.status === 'onboarding' ||
(connectAccount && !connectAccount.onboarding_complete);
const needsOnboarding = !connectAccount;
const getReturnUrls = () => {
const baseUrl = window.location.origin;
return {
refreshUrl: `${baseUrl}/payments?connect=refresh`,
returnUrl: `${baseUrl}/payments?connect=complete`,
};
};
const handleStartOnboarding = async () => {
setError(null);
try {
const { refreshUrl, returnUrl } = getReturnUrls();
const result = await onboardingMutation.mutateAsync({ refreshUrl, returnUrl });
// Redirect to Stripe onboarding
window.location.href = result.url;
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to start onboarding');
}
};
const handleRefreshLink = async () => {
setError(null);
try {
const { refreshUrl, returnUrl } = getReturnUrls();
const result = await refreshLinkMutation.mutateAsync({ refreshUrl, returnUrl });
// Redirect to continue onboarding
window.location.href = result.url;
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to refresh onboarding link');
}
};
// 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';
}
};
return (
<div className="space-y-6">
{/* Active Account Status */}
{isActive && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<CheckCircle className="text-green-600 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">
Your Stripe account is connected and ready to accept payments.
</p>
</div>
</div>
</div>
)}
{/* Account Details */}
{connectAccount && (
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium text-gray-900 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>
</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 ${
connectAccount.status === 'active'
? 'bg-green-100 text-green-800'
: connectAccount.status === 'onboarding'
? 'bg-yellow-100 text-yellow-800'
: connectAccount.status === 'restricted'
? 'bg-red-100 text-red-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{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">
{connectAccount.charges_enabled ? (
<>
<CreditCard size={14} className="text-green-600" />
<span className="text-green-600">Enabled</span>
</>
) : (
<>
<CreditCard size={14} className="text-gray-400" />
<span className="text-gray-500">Disabled</span>
</>
)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">Payouts:</span>
<span className="flex items-center gap-1">
{connectAccount.payouts_enabled ? (
<>
<Wallet size={14} className="text-green-600" />
<span className="text-green-600">Enabled</span>
</>
) : (
<>
<Wallet size={14} className="text-gray-400" />
<span className="text-gray-500">Disabled</span>
</>
)}
</span>
</div>
{connectAccount.stripe_account_id && (
<div className="flex justify-between">
<span className="text-gray-600">Account ID:</span>
<code className="font-mono text-gray-900 text-xs">
{connectAccount.stripe_account_id}
</code>
</div>
)}
</div>
</div>
)}
{/* Onboarding in Progress */}
{isOnboarding && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle className="text-yellow-600 shrink-0 mt-0.5" size={20} />
<div className="flex-1">
<h4 className="font-medium text-yellow-800">Complete Onboarding</h4>
<p className="text-sm text-yellow-700 mt-1">
Your Stripe Connect account setup is incomplete.
Click below to continue the onboarding process.
</p>
<button
onClick={handleRefreshLink}
disabled={refreshLinkMutation.isPending}
className="mt-3 flex items-center gap-2 px-4 py-2 text-sm font-medium text-yellow-800 bg-yellow-100 rounded-lg hover:bg-yellow-200 disabled:opacity-50"
>
{refreshLinkMutation.isPending ? (
<Loader2 size={16} className="animate-spin" />
) : (
<RefreshCw size={16} />
)}
Continue Onboarding
</button>
</div>
</div>
</div>
)}
{/* Start Onboarding */}
{needsOnboarding && (
<div className="space-y-4">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-800 mb-2">Connect with Stripe</h4>
<p className="text-sm text-blue-700">
As a {tier} tier business, you'll use Stripe Connect to accept payments.
This provides a seamless payment experience for your customers while
the platform handles payment processing.
</p>
<ul className="mt-3 space-y-1 text-sm text-blue-700">
<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>
<button
onClick={handleStartOnboarding}
disabled={onboardingMutation.isPending}
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] disabled:opacity-50"
>
{onboardingMutation.isPending ? (
<Loader2 size={18} className="animate-spin" />
) : (
<>
<ExternalLink size={18} />
Connect with Stripe
</>
)}
</button>
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start gap-2 text-red-800">
<AlertCircle size={18} className="shrink-0 mt-0.5" />
<span className="text-sm">{error}</span>
</div>
</div>
)}
{/* External Stripe Dashboard Link */}
{isActive && (
<a
href="https://dashboard.stripe.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
>
<ExternalLink size={14} />
Open Stripe Dashboard
</a>
)}
</div>
);
};
export default ConnectOnboarding;