import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/lib/api'; import { SettingsLayout } from './components/SettingsLayout'; import { SettingsCard } from './components/SettingsCard'; import { ToggleField } from './components/ToggleField'; import { GenericGatewayForm } from '@/components/settings/GenericGatewayForm'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle, GripVertical } from 'lucide-react'; import { toast } from 'sonner'; import { useMediaQuery } from '@/hooks/use-media-query'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; interface GatewayField { id: string; type: string; title: string; description: string; default: string | boolean; value: string | boolean; // Current saved value placeholder?: string; required: boolean; options?: Record; custom_attributes?: Record; } interface PaymentGateway { id: string; title: string; description: string; enabled: boolean; type: 'manual' | 'provider' | 'other'; icon: string; method_title: string; method_description: string; supports: string[]; requirements: { met: boolean; missing: string[]; }; settings: { basic: Record; api: Record; advanced: Record; }; has_fields: boolean; webhook_url: string | null; has_custom_ui: boolean; wc_settings_url: string; } // Sortable Gateway Item Component function SortableGatewayItem({ gateway, children }: { gateway: PaymentGateway; children: React.ReactNode }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: gateway.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
{/* Drag handle - hidden on mobile for better UX */}
{children}
); } export default function PaymentsPage() { const queryClient = useQueryClient(); const [selectedGateway, setSelectedGateway] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [togglingGateway, setTogglingGateway] = useState(null); const [manualOrder, setManualOrder] = useState([]); const [onlineOrder, setOnlineOrder] = useState([]); const isDesktop = useMediaQuery("(min-width: 768px)"); // DnD sensors const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // Fetch all payment gateways const { data: gatewayData, isLoading, refetch } = useQuery({ queryKey: ['payment-gateways'], queryFn: () => api.get('/payments/gateways'), refetchOnWindowFocus: true, staleTime: 5 * 60 * 1000, // 5 minutes }); // Extract gateways and order from response const gateways = gatewayData?.gateways || []; const savedOrder = gatewayData?.order || { manual: [], online: [] }; // Initialize order from saved order or gateways React.useEffect(() => { if (gateways.length > 0 && manualOrder.length === 0 && onlineOrder.length === 0) { // Use saved order if available, otherwise use gateway order if (savedOrder.manual.length > 0) { setManualOrder(savedOrder.manual); } else { const manual = gateways.filter((g: PaymentGateway) => g.type === 'manual').map((g: PaymentGateway) => g.id); setManualOrder(manual); } if (savedOrder.online.length > 0) { setOnlineOrder(savedOrder.online); } else { const online = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other').map((g: PaymentGateway) => g.id); setOnlineOrder(online); } } }, [gateways, savedOrder, manualOrder.length, onlineOrder.length]); // Toggle gateway mutation const toggleMutation = useMutation({ mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) => { setTogglingGateway(id); return api.post(`/payments/gateways/${id}/toggle`, { enabled }); }, onSuccess: async () => { // Wait for refetch to complete before showing toast await queryClient.invalidateQueries({ queryKey: ['payment-gateways'] }); toast.success('Gateway updated successfully'); setTogglingGateway(null); }, onError: () => { toast.error('Failed to update gateway'); setTogglingGateway(null); }, }); // Save gateway settings mutation const saveMutation = useMutation({ mutationFn: ({ id, settings }: { id: string; settings: Record }) => api.post(`/payments/gateways/${id}`, settings), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['payment-gateways'] }); toast.success('Settings saved successfully'); setIsModalOpen(false); setSelectedGateway(null); }, onError: () => { toast.error('Failed to save settings'); }, }); const handleToggle = (id: string, enabled: boolean) => { toggleMutation.mutate({ id, enabled }); }; const handleManageGateway = (gateway: PaymentGateway) => { setSelectedGateway(gateway); setIsModalOpen(true); }; // Handle drag end for manual gateways const handleManualDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = manualOrder.indexOf(active.id as string); const newIndex = manualOrder.indexOf(over.id as string); const newOrder = arrayMove(manualOrder, oldIndex, newIndex); setManualOrder(newOrder); // Save order to backend try { await api.post('/payments/gateways/order', { category: 'manual', order: newOrder, }); toast.success('Payment methods reordered'); } catch (error) { console.error('Failed to save order:', error); toast.error('Failed to save order'); // Revert order on error setManualOrder(manualOrder); } }; // Handle drag end for online gateways const handleOnlineDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = onlineOrder.indexOf(active.id as string); const newIndex = onlineOrder.indexOf(over.id as string); const newOrder = arrayMove(onlineOrder, oldIndex, newIndex); setOnlineOrder(newOrder); // Save order to backend try { await api.post('/payments/gateways/order', { category: 'online', order: newOrder, }); toast.success('Payment methods reordered'); } catch (error) { console.error('Failed to save order:', error); toast.error('Failed to save order'); // Revert order on error setOnlineOrder(onlineOrder); } }; const handleSaveGateway = async (settings: Record) => { if (!selectedGateway) return; await saveMutation.mutateAsync({ id: selectedGateway.id, settings }); }; // Separate and sort gateways by type and custom order const manualGateways = React.useMemo(() => { const manual = gateways.filter((g: PaymentGateway) => g.type === 'manual'); if (manualOrder.length === 0) return manual; return [...manual].sort((a, b) => { const indexA = manualOrder.indexOf(a.id); const indexB = manualOrder.indexOf(b.id); if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; }); }, [gateways, manualOrder]); const thirdPartyGateways = React.useMemo(() => { const online = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other'); if (onlineOrder.length === 0) return online; return [...online].sort((a, b) => { const indexA = onlineOrder.indexOf(a.id); const indexB = onlineOrder.indexOf(b.id); if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; }); }, [gateways, onlineOrder]); if (isLoading) { return (
); } return ( <> refetch()} disabled={isLoading} > Refresh } > {/* Manual Payment Methods - First priority */} {manualGateways.length === 0 ? (

No manual payment methods available

) : ( g.id)} strategy={verticalListSortingStrategy}>
{manualGateways.map((gateway: PaymentGateway) => (

{gateway.method_title || gateway.title}

{gateway.description && (

{gateway.description}

)}
{gateway.enabled && ( )} handleToggle(gateway.id, checked)} disabled={togglingGateway === gateway.id} />
))}
)}
{/* Online Payment Methods - Flat list */} {thirdPartyGateways.length > 0 && ( g.id)} strategy={verticalListSortingStrategy}>
{thirdPartyGateways.map((gateway: PaymentGateway) => (

{gateway.method_title || gateway.title}

{gateway.method_description && (

{gateway.method_description}

)} {!gateway.requirements.met && ( Requirements not met: {gateway.requirements.missing.join(', ')} )}
{gateway.enabled && ( )} handleToggle(gateway.id, checked)} disabled={!gateway.requirements.met || togglingGateway === gateway.id} />
))}
)}
{/* Gateway Settings Modal - Responsive: Dialog on desktop, Drawer on mobile */} {selectedGateway && isDesktop && ( {selectedGateway.title} Settings
setIsModalOpen(false)} hideFooter />
{/* Footer outside scrollable area */}
)} {selectedGateway && !isDesktop && ( {selectedGateway.title} Settings
setIsModalOpen(false)} hideFooter />
{/* Footer outside scrollable area */}
)} ); }