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>
270 lines
9.5 KiB
TypeScript
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;
|