/** * Stripe Settings Panel Component * * Comprehensive settings panel for Stripe Connect accounts. * Allows tenants to configure payout schedules, business profile, * branding, and view bank accounts. */ import React, { useState, useEffect } from 'react'; import { Calendar, Building2, Palette, Landmark, Loader2, AlertCircle, CheckCircle, ExternalLink, Save, RefreshCw, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useStripeSettings, useUpdateStripeSettings, useCreateConnectLoginLink } from '../hooks/usePayments'; import type { PayoutInterval, WeeklyAnchor, StripeSettingsUpdate, } from '../api/payments'; interface StripeSettingsPanelProps { stripeAccountId: string; } type TabId = 'payouts' | 'business' | 'branding' | 'bank'; const StripeSettingsPanel: React.FC = ({ stripeAccountId }) => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('payouts'); const [successMessage, setSuccessMessage] = useState(null); const { data: settings, isLoading, error, refetch } = useStripeSettings(); const updateMutation = useUpdateStripeSettings(); const loginLinkMutation = useCreateConnectLoginLink(); // Clear success message after 3 seconds useEffect(() => { if (successMessage) { const timer = setTimeout(() => setSuccessMessage(null), 3000); return () => clearTimeout(timer); } }, [successMessage]); // Handle opening Stripe Dashboard const handleOpenStripeDashboard = async () => { try { // Pass the current page URL as return/refresh URLs for Custom accounts const currentUrl = window.location.href; const result = await loginLinkMutation.mutateAsync({ return_url: currentUrl, refresh_url: currentUrl, }); if (result.type === 'login_link') { // Express accounts: Open dashboard in new tab (user stays there) window.open(result.url, '_blank'); } else { // Custom accounts: Navigate in same window (redirects back when done) window.location.href = result.url; } } catch { // Error is shown via mutation state } }; const tabs = [ { id: 'payouts' as TabId, label: t('payments.stripeSettings.payouts'), icon: Calendar }, { id: 'business' as TabId, label: t('payments.stripeSettings.businessProfile'), icon: Building2 }, { id: 'branding' as TabId, label: t('payments.stripeSettings.branding'), icon: Palette }, { id: 'bank' as TabId, label: t('payments.stripeSettings.bankAccounts'), icon: Landmark }, ]; if (isLoading) { return (
{t('payments.stripeSettings.loading')}
); } if (error) { return (

{t('payments.stripeSettings.loadError')}

{error instanceof Error ? error.message : t('payments.stripeSettings.unknownError')}

); } if (!settings) { return null; } const handleSave = async (updates: StripeSettingsUpdate) => { try { await updateMutation.mutateAsync(updates); setSuccessMessage(t('payments.stripeSettings.savedSuccessfully')); } catch { // Error is handled by mutation state } }; // For sub-tab links that need the static URL structure const stripeDashboardUrl = `https://dashboard.stripe.com/${stripeAccountId.startsWith('acct_') ? stripeAccountId : ''}`; return (
{/* Header with Stripe Dashboard link */}

{t('payments.stripeSettings.title')}

{t('payments.stripeSettings.description')}

{/* Login link error */} {loginLinkMutation.isError && (
{loginLinkMutation.error instanceof Error ? loginLinkMutation.error.message : t('payments.stripeSettings.loginLinkError')}
)} {/* Success message */} {successMessage && (
{successMessage}
)} {/* Error message */} {updateMutation.isError && (
{updateMutation.error instanceof Error ? updateMutation.error.message : t('payments.stripeSettings.saveError')}
)} {/* Tabs */}
{/* Tab content */}
{activeTab === 'payouts' && ( )} {activeTab === 'business' && ( )} {activeTab === 'branding' && ( )} {activeTab === 'bank' && ( )}
); }; // ============================================================================ // Payouts Tab // ============================================================================ interface PayoutsTabProps { settings: { schedule: { interval: PayoutInterval; delay_days: number; weekly_anchor: WeeklyAnchor | null; monthly_anchor: number | null; }; statement_descriptor: string; }; onSave: (updates: StripeSettingsUpdate) => Promise; isSaving: boolean; } const PayoutsTab: React.FC = ({ settings, onSave, isSaving }) => { const { t } = useTranslation(); const [interval, setInterval] = useState(settings.schedule.interval); const [delayDays, setDelayDays] = useState(settings.schedule.delay_days); const [weeklyAnchor, setWeeklyAnchor] = useState(settings.schedule.weekly_anchor); const [monthlyAnchor, setMonthlyAnchor] = useState(settings.schedule.monthly_anchor); const [statementDescriptor, setStatementDescriptor] = useState(settings.statement_descriptor); const [descriptorError, setDescriptorError] = useState(null); const weekDays: WeeklyAnchor[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; const validateDescriptor = (value: string) => { if (value.length > 22) { setDescriptorError(t('payments.stripeSettings.descriptorTooLong')); return false; } if (value && !/^[a-zA-Z0-9\s.\-]+$/.test(value)) { setDescriptorError(t('payments.stripeSettings.descriptorInvalidChars')); return false; } setDescriptorError(null); return true; }; const handleSave = async () => { if (!validateDescriptor(statementDescriptor)) return; const updates: StripeSettingsUpdate = { payouts: { schedule: { interval, delay_days: delayDays, ...(interval === 'weekly' && weeklyAnchor ? { weekly_anchor: weeklyAnchor } : {}), ...(interval === 'monthly' && monthlyAnchor ? { monthly_anchor: monthlyAnchor } : {}), }, ...(statementDescriptor ? { statement_descriptor: statementDescriptor } : {}), }, }; await onSave(updates); }; return (

{t('payments.stripeSettings.payoutsDescription')}

{/* Payout Schedule */}

{t('payments.stripeSettings.payoutSchedule')}

{/* Interval */}

{t('payments.stripeSettings.intervalHint')}

{/* Delay Days */}

{t('payments.stripeSettings.delayDaysHint')}

{/* Weekly Anchor */} {interval === 'weekly' && (
)} {/* Monthly Anchor */} {interval === 'monthly' && (
)}
{/* Statement Descriptor */}

{t('payments.stripeSettings.statementDescriptor')}

{ setStatementDescriptor(e.target.value); validateDescriptor(e.target.value); }} maxLength={22} placeholder={t('payments.stripeSettings.descriptorPlaceholder')} className={`w-full px-3 py-2 border rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent ${ descriptorError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' }`} /> {descriptorError ? (

{descriptorError}

) : (

{t('payments.stripeSettings.descriptorHint')} ({statementDescriptor.length}/22)

)}
{/* Save Button */}
); }; // ============================================================================ // Business Profile Tab // ============================================================================ interface BusinessProfileTabProps { settings: { name: string; support_email: string; support_phone: string; support_url: string; }; onSave: (updates: StripeSettingsUpdate) => Promise; isSaving: boolean; } const BusinessProfileTab: React.FC = ({ settings, onSave, isSaving }) => { const { t } = useTranslation(); const [name, setName] = useState(settings.name); const [supportEmail, setSupportEmail] = useState(settings.support_email); const [supportPhone, setSupportPhone] = useState(settings.support_phone); const [supportUrl, setSupportUrl] = useState(settings.support_url); const handleSave = async () => { const updates: StripeSettingsUpdate = { business_profile: { name, support_email: supportEmail, support_phone: supportPhone, support_url: supportUrl, }, }; await onSave(updates); }; return (

{t('payments.stripeSettings.businessProfileDescription')}

{/* Business Name */}
setName(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
{/* Support Email */}
setSupportEmail(e.target.value)} placeholder="support@yourbusiness.com" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />

{t('payments.stripeSettings.supportEmailHint')}

{/* Support Phone */}
setSupportPhone(e.target.value)} placeholder="+1 (555) 123-4567" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
{/* Support URL */}
setSupportUrl(e.target.value)} placeholder="https://yourbusiness.com/support" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />

{t('payments.stripeSettings.supportUrlHint')}

{/* Save Button */}
); }; // ============================================================================ // Branding Tab // ============================================================================ interface BrandingTabProps { settings: { primary_color: string; secondary_color: string; icon: string; logo: string; }; onSave: (updates: StripeSettingsUpdate) => Promise; isSaving: boolean; stripeDashboardUrl: string; } const BrandingTab: React.FC = ({ settings, onSave, isSaving, stripeDashboardUrl }) => { const { t } = useTranslation(); const [primaryColor, setPrimaryColor] = useState(settings.primary_color || '#3b82f6'); const [secondaryColor, setSecondaryColor] = useState(settings.secondary_color || '#10b981'); const [colorError, setColorError] = useState(null); const validateColor = (color: string): boolean => { if (!color) return true; return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color); }; const handleSave = async () => { if (primaryColor && !validateColor(primaryColor)) { setColorError(t('payments.stripeSettings.invalidColorFormat')); return; } if (secondaryColor && !validateColor(secondaryColor)) { setColorError(t('payments.stripeSettings.invalidColorFormat')); return; } setColorError(null); const updates: StripeSettingsUpdate = { branding: { primary_color: primaryColor, secondary_color: secondaryColor, }, }; await onSave(updates); }; return (

{t('payments.stripeSettings.brandingDescription')}

{colorError && (

{colorError}

)}
{/* Primary Color */}
setPrimaryColor(e.target.value)} className="h-10 w-14 rounded border border-gray-300 dark:border-gray-600 cursor-pointer" /> setPrimaryColor(e.target.value)} placeholder="#3b82f6" className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
{/* Secondary Color */}
setSecondaryColor(e.target.value)} className="h-10 w-14 rounded border border-gray-300 dark:border-gray-600 cursor-pointer" /> setSecondaryColor(e.target.value)} placeholder="#10b981" className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
{/* Logo & Icon Info */}

{t('payments.stripeSettings.logoAndIcon')}

{t('payments.stripeSettings.logoAndIconDescription')}

{t('payments.stripeSettings.uploadInStripeDashboard')} {/* Display current logo/icon if set */} {(settings.icon || settings.logo) && (
{settings.icon && (

{t('payments.stripeSettings.icon')}

)} {settings.logo && (

{t('payments.stripeSettings.logo')}

)}
)}
{/* Save Button */}
); }; // ============================================================================ // Bank Accounts Tab // ============================================================================ interface BankAccountsTabProps { accounts: Array<{ id: string; bank_name: string; last4: string; currency: string; default_for_currency: boolean; status: string; }>; stripeDashboardUrl: string; } const BankAccountsTab: React.FC = ({ accounts, stripeDashboardUrl }) => { const { t } = useTranslation(); return (

{t('payments.stripeSettings.bankAccountsDescription')}

{accounts.length === 0 ? (

{t('payments.stripeSettings.noBankAccounts')}

{t('payments.stripeSettings.noBankAccountsDescription')}

{t('payments.stripeSettings.addInStripeDashboard')}
) : (
{accounts.map((account) => (

{account.bank_name || t('payments.stripeSettings.bankAccount')}

••••{account.last4} · {account.currency.toUpperCase()}

{account.default_for_currency && ( {t('payments.stripeSettings.default')} )} {account.status}
))}
)}
); }; export default StripeSettingsPanel;