From 213870a4e22d3d6b85e952686709bd96f4d725cb Mon Sep 17 00:00:00 2001 From: dwindown Date: Wed, 5 Nov 2025 21:19:53 +0700 Subject: [PATCH] feat: Connect Payments page to real WooCommerce API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Phase 1 Frontend Complete! 🎨 Payments.tsx - Complete Rewrite: - Replaced mock data with real API calls - useQuery to fetch gateways from /payments/gateways - useMutation for toggle and save operations - Optimistic updates for instant UI feedback - Refetch on window focus (5 min stale time) - Manual refresh button - Loading states with spinner - Empty states with helpful messages - Error handling with toast notifications 🏗️ Gateway Categorization: - Manual methods (Bank Transfer, COD, Check) - Payment providers (Stripe, PayPal, etc.) - Other WC-compliant gateways - Auto-discovers all installed gateways 🎯 Features: - Enable/disable toggle with optimistic updates - Manage button opens settings modal - GenericGatewayForm for configuration - Requirements checking (SSL, extensions) - Link to WC settings for complex cases - Responsive design - Keyboard accessible 📋 Checklist Progress: - [x] PaymentGatewaysProvider.php - [x] PaymentsController.php - [x] GenericGatewayForm.tsx - [x] Update Payments.tsx with real API - [ ] Test with real WooCommerce (next) 🎉 Backend + Frontend integration complete! Ready for testing with actual WooCommerce installation. --- .../settings/GenericGatewayForm.tsx | 6 +- admin-spa/src/routes/Settings/Payments.tsx | 513 +++++++++++------- 2 files changed, 319 insertions(+), 200 deletions(-) diff --git a/admin-spa/src/components/settings/GenericGatewayForm.tsx b/admin-spa/src/components/settings/GenericGatewayForm.tsx index 71915ce..afdca97 100644 --- a/admin-spa/src/components/settings/GenericGatewayForm.tsx +++ b/admin-spa/src/components/settings/GenericGatewayForm.tsx @@ -37,7 +37,11 @@ interface GenericGatewayFormProps { gateway: { id: string; title: string; - settings: GatewaySettings; + settings: { + basic: Record; + api: Record; + advanced: Record; + }; wc_settings_url: string; }; onSave: (settings: Record) => Promise; diff --git a/admin-spa/src/routes/Settings/Payments.tsx b/admin-spa/src/routes/Settings/Payments.tsx index 64d9b73..9fb1937 100644 --- a/admin-spa/src/routes/Settings/Payments.tsx +++ b/admin-spa/src/routes/Settings/Payments.tsx @@ -1,237 +1,352 @@ 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 { CreditCard, DollarSign, Banknote, Settings } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle } from 'lucide-react'; import { toast } from 'sonner'; -interface PaymentProvider { +interface GatewayField { id: string; - name: string; + type: string; + title: string; + description: string; + default: string | boolean; + placeholder?: string; + required: boolean; + options?: Record; + custom_attributes?: Record; +} + +interface PaymentGateway { + id: string; + title: string; description: string; - icon: React.ReactNode; enabled: boolean; - connected: boolean; - fees?: string; - testMode?: 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; } export default function PaymentsPage() { - const [testMode, setTestMode] = useState(false); - const [providers] = useState([ - { - id: 'stripe', - name: 'Stripe Payments', - description: 'Accept Visa, Mastercard, Amex, and more', - icon: , - enabled: false, - connected: false, - fees: '2.9% + $0.30 per transaction', - }, - { - id: 'paypal', - name: 'PayPal', - description: 'Accept PayPal payments worldwide', - icon: , - enabled: true, - connected: true, - fees: '3.49% + fixed fee per transaction', - }, - ]); + const queryClient = useQueryClient(); + const [selectedGateway, setSelectedGateway] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); - const [manualMethods, setManualMethods] = useState([ - { id: 'bacs', name: 'Bank Transfer (BACS)', enabled: true }, - { id: 'cod', name: 'Cash on Delivery', enabled: true }, - { id: 'cheque', name: 'Check Payments', enabled: false }, - ]); + // Fetch all payment gateways + const { data: gateways = [], isLoading, refetch } = useQuery({ + queryKey: ['payment-gateways'], + queryFn: () => api.get('/payments/gateways'), + refetchOnWindowFocus: true, + staleTime: 5 * 60 * 1000, // 5 minutes + }); - const handleSave = async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - toast.success('Payment settings have been updated successfully.'); + // Toggle gateway mutation + const toggleMutation = useMutation({ + mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) => + api.post(`/payments/gateways/${id}/toggle`, { enabled }), + onMutate: async ({ id, enabled }) => { + // Optimistic update + await queryClient.cancelQueries({ queryKey: ['payment-gateways'] }); + const previous = queryClient.getQueryData(['payment-gateways']); + + queryClient.setQueryData(['payment-gateways'], (old: PaymentGateway[]) => + old.map((g) => (g.id === id ? { ...g, enabled } : g)) + ); + + return { previous }; + }, + onError: (_err, _variables, context) => { + queryClient.setQueryData(['payment-gateways'], context?.previous); + toast.error('Failed to update gateway'); + }, + onSuccess: () => { + toast.success('Gateway updated successfully'); + }, + }); + + // 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); + }; - const toggleManualMethod = (id: string) => { - setManualMethods((prev) => - prev.map((m) => (m.id === id ? { ...m, enabled: !m.enabled } : m)) + const handleSaveGateway = async (settings: Record) => { + if (!selectedGateway) return; + await saveMutation.mutateAsync({ id: selectedGateway.id, settings }); + }; + + // Categorize gateways + const manualGateways = gateways.filter((g: PaymentGateway) => g.type === 'manual'); + const providerGateways = gateways.filter((g: PaymentGateway) => g.type === 'provider'); + const otherGateways = gateways.filter((g: PaymentGateway) => g.type === 'other'); + + if (isLoading) { + return ( + +
+ +
+
); - }; + } return ( - - {/* Manual Payment Methods - First priority */} - -
- {manualMethods.map((method) => ( -
-
-
-
- -
-
-

{method.name}

- {method.enabled && ( -

- Customers can choose this at checkout -

- )} -
-
-
- {method.enabled && ( - - )} - toggleManualMethod(method.id)} - /> -
-
-
- ))} + <> + + {/* Refresh button */} +
+
- - {/* Payment Providers - Second priority */} - -
- {providers.map((provider) => ( -
-
-
-
- {provider.icon} -
-
-
-

{provider.name}

- {provider.connected ? ( - - ● Connected - - ) : ( - ○ Not connected - )} + {/* Manual Payment Methods - First priority */} + + {manualGateways.length === 0 ? ( +

No manual payment methods available

+ ) : ( +
+ {manualGateways.map((gateway: PaymentGateway) => ( +
+
+
+
+ +
+
+

{gateway.title}

+ {gateway.description && ( +

+ {gateway.description} +

+ )} +
+
+
+ {gateway.enabled && ( + + )} + handleToggle(gateway.id, checked)} + />
-

- {provider.description} -

- {provider.fees && ( -

- {provider.fees} -

- )}
-
- {provider.connected ? ( - <> -
+ )} + + + {/* Payment Providers - Second priority */} + + {providerGateways.length === 0 ? ( +

+ No payment providers installed.{' '} + + Browse payment gateways + + +

+ ) : ( +
+ {providerGateways.map((gateway: PaymentGateway) => ( +
+
+
+
+ +
+
+
+

{gateway.title}

+ {gateway.enabled ? ( + + ● Enabled + + ) : ( + ○ Disabled + )} +
+

+ {gateway.description} +

+ {!gateway.requirements.met && ( + + + + Requirements not met: {gateway.requirements.missing.join(', ')} + + + )} +
+
+
+ - - - ) : ( - - )} + handleToggle(gateway.id, checked)} + disabled={!gateway.requirements.met} + /> +
+
-
+ ))}
- ))} + )} +
- -
- - - {/* Payment Settings - Third priority (test mode, capture, etc) */} - - {/* Test Mode Banner */} - {testMode && ( -
-
- - ⚠️ Test Mode Active - - - No real charges will be processed - + {/* Other Gateways */} + {otherGateways.length > 0 && ( + +
+ {otherGateways.map((gateway: PaymentGateway) => ( +
+
+
+
+ +
+
+

{gateway.title}

+ {gateway.description && ( +

+ {gateway.description} +

+ )} +
+
+
+ + handleToggle(gateway.id, checked)} + /> +
+
+
+ ))}
-
+ )} + - - -
-
- -

- Choose when to capture payment from customers -

-
- - -
-
-
- - - {/* Help Card */} -
-

💡 Need help setting up payments?

-

- Our setup wizard can help you connect Stripe or PayPal in minutes. -

- -
- + {/* Gateway Settings Modal */} + {selectedGateway && ( + + + + {selectedGateway.title} Settings + + setIsModalOpen(false)} + /> + + + )} + ); }