/** * 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, useEffect, useRef } 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 { useTranslation } from 'react-i18next'; import { createAccountSession, refreshConnectStatus, ConnectAccountInfo } from '../api/payments'; import { useDarkMode } from '../hooks/useDarkMode'; // Get appearance config based on dark mode const getAppearance = (isDark: boolean) => ({ overlays: 'drawer' as const, variables: { // Brand colors - using your blue theme colorPrimary: '#3b82f6', // brand-500 colorBackground: isDark ? '#1f2937' : '#ffffff', // gray-800 / white colorText: isDark ? '#f9fafb' : '#111827', // gray-50 / gray-900 colorSecondaryText: isDark ? '#9ca3af' : '#6b7280', // gray-400 / gray-500 colorBorder: isDark ? '#374151' : '#e5e7eb', // gray-700 / gray-200 colorDanger: '#ef4444', // red-500 // Typography - matching Inter font fontFamily: 'Inter, system-ui, -apple-system, sans-serif', fontSizeBase: '14px', fontSizeSm: '12px', fontSizeLg: '16px', fontSizeXl: '18px', fontWeightNormal: '400', fontWeightMedium: '500', fontWeightBold: '600', // Spacing & Borders - matching your rounded-lg style spacingUnit: '12px', borderRadius: '8px', // Form elements formBackgroundColor: isDark ? '#111827' : '#f9fafb', // gray-900 / gray-50 formBorderColor: isDark ? '#374151' : '#d1d5db', // gray-700 / gray-300 formHighlightColorBorder: '#3b82f6', // brand-500 formAccentColor: '#3b82f6', // brand-500 // Buttons buttonPrimaryColorBackground: '#3b82f6', // brand-500 buttonPrimaryColorText: '#ffffff', buttonSecondaryColorBackground: isDark ? '#374151' : '#f3f4f6', // gray-700 / gray-100 buttonSecondaryColorText: isDark ? '#f9fafb' : '#374151', // gray-50 / gray-700 buttonSecondaryColorBorder: isDark ? '#4b5563' : '#d1d5db', // gray-600 / gray-300 // Action colors actionPrimaryColorText: '#3b82f6', // brand-500 actionSecondaryColorText: isDark ? '#9ca3af' : '#6b7280', // gray-400 / gray-500 // Badge colors badgeNeutralColorBackground: isDark ? '#374151' : '#f3f4f6', // gray-700 / gray-100 badgeNeutralColorText: isDark ? '#d1d5db' : '#4b5563', // gray-300 / gray-600 badgeSuccessColorBackground: isDark ? '#065f46' : '#d1fae5', // green-800 / green-100 badgeSuccessColorText: isDark ? '#6ee7b7' : '#065f46', // green-300 / green-800 badgeWarningColorBackground: isDark ? '#92400e' : '#fef3c7', // amber-800 / amber-100 badgeWarningColorText: isDark ? '#fcd34d' : '#92400e', // amber-300 / amber-800 badgeDangerColorBackground: isDark ? '#991b1b' : '#fee2e2', // red-800 / red-100 badgeDangerColorText: isDark ? '#fca5a5' : '#991b1b', // red-300 / red-800 // Offset background (used for layered sections) offsetBackgroundColor: isDark ? '#111827' : '#f9fafb', // gray-900 / gray-50 }, }); interface ConnectOnboardingEmbedProps { connectAccount: ConnectAccountInfo | null; tier: string; onComplete?: () => void; onError?: (error: string) => void; } type LoadingState = 'idle' | 'loading' | 'ready' | 'error' | 'complete'; const ConnectOnboardingEmbed: React.FC = ({ connectAccount, tier, onComplete, onError, }) => { const { t } = useTranslation(); const isDark = useDarkMode(); const [stripeConnectInstance, setStripeConnectInstance] = useState(null); const [loadingState, setLoadingState] = useState('idle'); const [errorMessage, setErrorMessage] = useState(null); // Track the theme that was used when initializing const initializedThemeRef = useRef(null); // Flag to trigger auto-reinitialize const [needsReinit, setNeedsReinit] = useState(false); const isActive = connectAccount?.status === 'active' && connectAccount?.charges_enabled; // Detect theme changes when onboarding is already open useEffect(() => { if (loadingState === 'ready' && initializedThemeRef.current !== null && initializedThemeRef.current !== isDark) { // Theme changed while onboarding is open - trigger reinitialize setNeedsReinit(true); } }, [isDark, loadingState]); // Handle reinitialization useEffect(() => { if (needsReinit) { setStripeConnectInstance(null); initializedThemeRef.current = null; setNeedsReinit(false); // Re-run initialization (async () => { setLoadingState('loading'); setErrorMessage(null); try { const response = await createAccountSession(); const { client_secret, publishable_key } = response.data; const instance = await loadConnectAndInitialize({ publishableKey: publishable_key, fetchClientSecret: async () => client_secret, appearance: getAppearance(isDark), }); setStripeConnectInstance(instance); setLoadingState('ready'); initializedThemeRef.current = isDark; } catch (err: any) { console.error('Failed to reinitialize Stripe Connect:', err); const message = err.response?.data?.error || err.message || t('payments.failedToInitializePayment'); setErrorMessage(message); setLoadingState('error'); onError?.(message); } })(); } }, [needsReinit, isDark, t, onError]); // Initialize Stripe Connect (user-triggered) 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 with theme-aware appearance const instance = await loadConnectAndInitialize({ publishableKey: publishable_key, fetchClientSecret: async () => client_secret, appearance: getAppearance(isDark), }); setStripeConnectInstance(instance); setLoadingState('ready'); initializedThemeRef.current = isDark; } catch (err: any) { console.error('Failed to initialize Stripe Connect:', err); const message = err.response?.data?.error || err.message || t('payments.failedToInitializePayment'); setErrorMessage(message); setLoadingState('error'); onError?.(message); } }, [loadingState, onError, t, isDark]); // 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 || t('payments.failedToLoadPaymentComponent'); setErrorMessage(message); setLoadingState('error'); onError?.(message); }, [onError, t]); // 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'); } }; // If account is already active, show status if (isActive) { return (

{t('payments.stripeConnected')}

{t('payments.stripeConnectedDesc')}

{t('payments.accountDetails')}

{t('payments.accountType')}: {getAccountTypeLabel()}
{t('payments.status')}: {connectAccount.status}
{t('payments.charges')}: {t('payments.enabled')}
{t('payments.payouts')}: {connectAccount.payouts_enabled ? t('payments.enabled') : t('payments.pending')}
); } // Completion state if (loadingState === 'complete') { return (

{t('payments.onboardingComplete')}

{t('payments.stripeSetupComplete')}

); } // Error state if (loadingState === 'error') { return (

{t('payments.setupFailed')}

{errorMessage}

); } // Idle state - show start button if (loadingState === 'idle') { return (

{t('payments.setUpPayments')}

{t('payments.tierPaymentDescriptionWithOnboarding', { tier })}

  • {t('payments.securePaymentProcessing')}
  • {t('payments.automaticPayouts')}
  • {t('payments.pciCompliance')}
); } // Loading state if (loadingState === 'loading') { return (

{t('payments.initializingPaymentSetup')}

); } // Ready state - show embedded onboarding if (loadingState === 'ready' && stripeConnectInstance) { return (

{t('payments.completeAccountSetup')}

{t('payments.fillOutInfoForPayment')}

); } return null; }; export default ConnectOnboardingEmbed;