Translate all hardcoded English strings to use i18n translation keys: Components: - TransactionDetailModal: payment details, refunds, technical info - ConnectOnboarding/ConnectOnboardingEmbed: Stripe Connect setup - StripeApiKeysForm: API key management - DomainPurchase: domain registration flow - Sidebar: navigation labels - Schedule/Sidebar, PendingSidebar: scheduler UI - MasqueradeBanner: masquerade status - Dashboard widgets: metrics, capacity, customers, tickets - Marketing: PricingTable, PluginShowcase, BenefitsSection - ConfirmationModal, ServiceList: common UI Pages: - Staff: invitation flow, role management - Customers: form placeholders - Payments: transactions, payouts, billing - BookingSettings: URL and redirect configuration - TrialExpired: upgrade prompts and features - PlatformSettings, PlatformBusinesses: admin UI - HelpApiDocs: API documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
269 lines
9.6 KiB
TypeScript
269 lines
9.6 KiB
TypeScript
/**
|
|
* Stripe Connect Onboarding Component
|
|
* For paid-tier businesses to connect their Stripe account via Connect
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
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 { t } = useTranslation();
|
|
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 || t('payments.failedToStartOnboarding'));
|
|
}
|
|
};
|
|
|
|
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 || t('payments.failedToRefreshLink'));
|
|
}
|
|
};
|
|
|
|
// Account type display
|
|
const getAccountTypeLabel = () => {
|
|
switch (connectAccount?.account_type) {
|
|
case 'standard':
|
|
return t('payments.standardConnect');
|
|
case 'express':
|
|
return t('payments.expressConnect');
|
|
case 'custom':
|
|
return t('payments.customConnect');
|
|
default:
|
|
return t('payments.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">{t('payments.stripeConnected')}</h4>
|
|
<p className="text-sm text-green-700 mt-1">
|
|
{t('payments.stripeConnectedDesc')}
|
|
</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">{t('payments.accountDetails')}</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">{t('payments.accountType')}:</span>
|
|
<span className="text-gray-900">{getAccountTypeLabel()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">{t('payments.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">{t('payments.charges')}:</span>
|
|
<span className="flex items-center gap-1">
|
|
{connectAccount.charges_enabled ? (
|
|
<>
|
|
<CreditCard size={14} className="text-green-600" />
|
|
<span className="text-green-600">{t('payments.enabled')}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<CreditCard size={14} className="text-gray-400" />
|
|
<span className="text-gray-500">{t('payments.disabled')}</span>
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-gray-600">{t('payments.payouts')}:</span>
|
|
<span className="flex items-center gap-1">
|
|
{connectAccount.payouts_enabled ? (
|
|
<>
|
|
<Wallet size={14} className="text-green-600" />
|
|
<span className="text-green-600">{t('payments.enabled')}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Wallet size={14} className="text-gray-400" />
|
|
<span className="text-gray-500">{t('payments.disabled')}</span>
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
{connectAccount.stripe_account_id && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">{t('payments.accountId')}:</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">{t('payments.completeOnboarding')}</h4>
|
|
<p className="text-sm text-yellow-700 mt-1">
|
|
{t('payments.onboardingIncomplete')}
|
|
</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} />
|
|
)}
|
|
{t('payments.continueOnboarding')}
|
|
</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">{t('payments.connectWithStripe')}</h4>
|
|
<p className="text-sm text-blue-700">
|
|
{t('payments.tierPaymentDescription', { tier })}
|
|
</p>
|
|
<ul className="mt-3 space-y-1 text-sm text-blue-700">
|
|
<li className="flex items-center gap-2">
|
|
<CheckCircle size={14} />
|
|
{t('payments.securePaymentProcessing')}
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<CheckCircle size={14} />
|
|
{t('payments.automaticPayouts')}
|
|
</li>
|
|
<li className="flex items-center gap-2">
|
|
<CheckCircle size={14} />
|
|
{t('payments.pciCompliance')}
|
|
</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} />
|
|
{t('payments.connectWithStripe')}
|
|
</>
|
|
)}
|
|
</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} />
|
|
{t('payments.openStripeDashboard')}
|
|
</a>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ConnectOnboarding;
|