From e57f17047020279c521f6663e8b4c55e58d4ed4f Mon Sep 17 00:00:00 2001 From: Mitchell Magro Date: Tue, 30 Dec 2025 18:27:24 +0100 Subject: [PATCH] Added dynamic forms --- src/features/cashier/Cashier.tsx | 39 ++- .../cashier/PaymentForm/PaymentForm.tsx | 301 +++++++++++------- .../cashier/context/CashierContext.tsx | 21 +- src/features/cashier/hooks/useCurrencies.ts | 29 -- src/features/cashier/hooks/usePayment.ts | 43 --- .../cashier/hooks/usePaymentMethods.ts | 29 -- .../cashier/services/CashierService.ts | 48 ++- src/features/cashier/types.ts | 14 +- src/features/cashier/utils/formBuilder.ts | 114 +++++++ src/services/api.ts | 28 +- 10 files changed, 430 insertions(+), 236 deletions(-) delete mode 100644 src/features/cashier/hooks/useCurrencies.ts delete mode 100644 src/features/cashier/hooks/usePayment.ts delete mode 100644 src/features/cashier/hooks/usePaymentMethods.ts create mode 100644 src/features/cashier/utils/formBuilder.ts diff --git a/src/features/cashier/Cashier.tsx b/src/features/cashier/Cashier.tsx index 8262139..cc1f413 100644 --- a/src/features/cashier/Cashier.tsx +++ b/src/features/cashier/Cashier.tsx @@ -8,6 +8,7 @@ import type { IPaymentMethod } from '@/features/payment-methods/types'; import type { IPaymentRequest } from './types'; import { useCashier } from './context/CashierContext'; import { getCashierConfig } from '@/config/cashierConfig'; +import { cashierService } from './services/CashierService'; function Cashier() { const { @@ -15,8 +16,10 @@ function Cashier() { methods, currencies, selectedMethod, + formMetadata, error, paymentUrl, + goBack, setSelectedMethod, initiatePayment, } = useCashier(); @@ -32,9 +35,38 @@ function Cashier() { setSelectedMethod(method); }; - const handleFormSubmit = async (formData: Omit) => { + const handleFormSubmit = async (formData: Record) => { try { - await initiatePayment(formData); + // Transform form data to match IPaymentRequest structure + const additionalFields: Record = {}; + if (formData.account) { + additionalFields.account = formData.account as string; + } + + // Preserve any additional fields + Object.entries(formData).forEach(([key, value]) => { + if ( + !['payment_type', 'method', 'currency', 'amount', 'customer', 'redirect', 'account'].includes(key) + ) { + additionalFields[key] = value; + } + }); + + const paymentRequest: Omit = { + payment_type: (formData.payment_type as 'deposit' | 'withdrawal') || 'deposit', + method: (formData.method as string) || '', + currency: (formData.currency as string) || '', + amount: (formData.amount as number) || 0, + customer: (formData.customer as IPaymentRequest['customer']) || { + id: '', + first_name: '', + last_name: '', + email: '', + }, + redirect: formData.redirect as IPaymentRequest['redirect'], + ...additionalFields, + }; + await initiatePayment(paymentRequest); } catch { // Error is handled by the service state } @@ -49,7 +81,7 @@ function Cashier() { } if (state === 'error' && error) { - return ; + return goBack()}/>; } if (state === 'redirecting') { @@ -61,6 +93,7 @@ function Cashier() { void; + onSubmit: (data: Record) => void; onBack: () => void; isLoading?: boolean; } @@ -29,31 +30,151 @@ interface IPaymentFormProps { function PaymentForm({ method, currencies, + formMetadata, config, onSubmit, onBack, isLoading = false, }: IPaymentFormProps) { - const [paymentType, setPaymentType] = useState<'deposit' | 'withdrawal'>( - config?.paymentType || 'deposit' - ); - const [currency, setCurrency] = useState(config?.currency || currencies[0]?.code || ''); - const [amount, setAmount] = useState(''); - const [account, setAccount] = useState(config.account || ''); + const [formData, setFormData] = useState>(() => { + const initial: Record = { + type: config?.paymentType || 'deposit', + method: method.code, + currency: config?.currency || currencies[0]?.code || '', + amount: '', + }; + + // Initialize customer fields from config + if (config.customer) { + initial.customer = { ...config.customer }; + } + + return initial; + }); + + const parsedFields = useMemo(() => { + if (formMetadata?. length === 0) return []; + return parseFormFields(formMetadata); + }, [formMetadata]); + + const visibleFields = useMemo(() => { + return parsedFields?.filter(field => + shouldShowField(field, { + paymentType: config?.paymentType, + currency: config?.currency, + }) + ); + }, [parsedFields, config]); + + const groupedFields = useMemo(() => { + return groupFieldsBySection(visibleFields); + }, [visibleFields]); + + const getFieldValue = (field: IParsedField): string => { + let current: unknown = formData; + for (const key of field.path) { + if (typeof current === 'object' && current !== null && key in current) { + current = (current as Record)[key]; + } else { + return ''; + } + } + return String(current || ''); + }; + + const setFieldValue = (field: IParsedField, value: string): void => { + const nestedObj = buildNestedObject(field.path, value); + setFormData(prev => mergeNestedObjects(prev, nestedObj)); + }; + + const renderField = (field: IParsedField) => { + const value = getFieldValue(field); + const fieldId = `field-${field.code}`; + + if (field.inputType === 'radio' && field.code === 'type') { + return ( +
+ + +
+ ); + } + + if (field.inputType === 'select' && field.code === 'currency') { + return ( +
+ + +
+ ); + } + + const inputProps: Record = { + id: fieldId, + className: bem(block, 'input'), + value, + onChange: (e: React.ChangeEvent) => setFieldValue(field, e.target.value), + required: true, + placeholder: `Enter ${field.name.toLowerCase()}`, + }; + + if (field.inputType === 'number') { + inputProps.type = 'number'; + inputProps.step = '0.01'; + inputProps.min = '0'; + } else { + inputProps.type = field.inputType; + } + + if (field.inputType === 'date') { + inputProps.type = 'date'; + } + + return ( +
+ + +
+ ); + }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); - // Build customer object from config (all fields required: id, first_name, last_name, email) - const customer: ICustomer = { - id: config.customer?.id || '', - first_name: 'John', - last_name: 'Doe', - email: 'john.doe@example.com', - }; - - // Build redirect URLs (use config or defaults) - // Convert relative URLs to absolute URLs for payment provider redirects + // Build redirect URLs const defaultRedirects = getDefaultRedirectUrls(); const redirect: IRedirect = { success: normalizeRedirectUrl(config.redirect?.success || defaultRedirects.success), @@ -61,34 +182,26 @@ function PaymentForm({ error: normalizeRedirectUrl(config.redirect?.error || defaultRedirects.error), }; - const submitData: { - payment_type: 'deposit' | 'withdrawal'; - method: string; - currency: string; - amount: number; - customer: ICustomer; - redirect: IRedirect; - account?: string; - } = { - payment_type: paymentType, + // Build submission data + const submitData: Record = { + ...formData, + payment_type: formData.type, method: method.code, - currency, - amount: parseFloat(amount), - customer, redirect, }; - // Include account for withdrawals - if (paymentType === 'withdrawal') { - submitData.account = account || config.account; + // Convert amount to number if it exists + if (submitData.amount && typeof submitData.amount === 'string') { + submitData.amount = parseFloat(submitData.amount); } + // Remove type field (use payment_type instead) + delete submitData.type; + onSubmit(submitData); }; - const showPaymentType = !config.paymentType; - const showCurrency = !config.currency; - const showAccount = paymentType === 'withdrawal' && !config.account; + const showPaymentType = !config.paymentType && groupedFields.payment.some(f => f.code === 'type'); return (
@@ -101,87 +214,35 @@ function PaymentForm({
- {showPaymentType && ( + {showPaymentType && groupedFields.payment.some(f => f.code === 'type') && (

Payment Type

-
- - -
+ {groupedFields.payment.filter(f => f.code === 'type').map(renderField)}
)} -
-

Payment Information

- - {showCurrency && ( -
- - -
- )} - -
- - setAmount(e.target.value)} - step="0.01" - min="0" - required - placeholder="Enter amount" - /> + {groupedFields.payment.filter(f => f.code !== 'type' && f.code !== 'method').length > 0 && ( +
+

Payment Information

+ {groupedFields.payment + .filter(f => f.code !== 'type' && f.code !== 'method') + .map(renderField)}
+ )} - {showAccount && ( -
- - setAccount(e.target.value)} - required - placeholder="Enter IBAN or account number" - /> -
- )} -
+ {groupedFields.customer.length > 0 && ( +
+

Customer Information

+ {groupedFields.customer.map(renderField)} +
+ )} + + {groupedFields.account.length > 0 && ( +
+

Account Information

+ {groupedFields.account.map(renderField)} +
+ )}