diff --git a/src/api/forms.ts b/src/api/forms.ts index 3d1d147a..0248f1a3 100644 --- a/src/api/forms.ts +++ b/src/api/forms.ts @@ -23,6 +23,23 @@ export interface FieldValidation { } // Individual form field definition with all possible properties +export interface PaymentOptions { + amount: number + currency?: string + description?: string + // Merchant / branding + businessName?: string // Shown as merchant name in Razorpay modal + logoUrl?: string // Merchant logo URL (optional) + themeColor?: string // Hex color for Razorpay modal (#rrggbb) + // Prefill respondent info + prefillName?: string + prefillEmail?: string + prefillContact?: string + // Receipt/notes + receiptPrefix?: string // e.g. "invoice", "order" — prefixed to receipt ID + notes?: string // Payment notes visible on Razorpay dashboard +} + export interface FormField { id: string // Unique field identifier fieldName: string // Internal field name (used in API) @@ -37,7 +54,7 @@ export interface FormField { min?: number // Minimum value for numeric fields max?: number // Maximum value for numeric fields step?: number // Step increment for numeric fields - options?: Array // Options for select/radio/checkbox fields + options?: Array | PaymentOptions // Options for select/radio/checkbox/payment fields } // Form definition containing metadata and optional fields @@ -86,20 +103,7 @@ export interface CreateFieldInput { min?: number max?: number step?: number - options?: Array -} - -export interface UpdateFieldInput { - fieldName?: string - label?: string - fieldValueType?: string - fieldType?: string - validation?: FieldValidation - placeholder?: string - min?: number - max?: number - step?: number - options?: Array + options?: Array | PaymentOptions } // Base URL for API endpoints diff --git a/src/api/payments.ts b/src/api/payments.ts new file mode 100644 index 00000000..c58b4a21 --- /dev/null +++ b/src/api/payments.ts @@ -0,0 +1,118 @@ +/** + * Payments API Layer (Razorpay) + * + * Handles all payment-related API calls: + * - Creating Razorpay orders + * - Verifying payment signatures + * - Checking payment status + * - Getting Razorpay key for frontend checkout + */ + +export interface CreateOrderResponse { + orderId: string + amount: number + currency: string + keyId: string +} + +export interface VerifyPaymentResponse { + orderId: string + paymentId: string + status: string +} + +export interface PaymentStatus { + status: string + amount: number + currency: string +} + +export interface RazorpayConfig { + keyId: string +} + +interface ApiResponse { + success: boolean + message?: string + data: T +} + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' + +async function handleResponse(response: Response): Promise { + const result: ApiResponse = await response.json() + if (!response.ok || !result.success) { + throw new Error(result.message || `Request failed: ${response.statusText}`) + } + return result.data +} + +export const paymentsApi = { + /** + * Create a Razorpay order for a payment field + */ + createOrder: async ( + formId: string, + fieldId: string, + params: { + amount: number + currency?: string + responseId?: string + }, + ): Promise => { + const response = await fetch( + `${API_URL}/payments/order/${formId}/${fieldId}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + credentials: 'include', + }, + ) + return handleResponse(response) + }, + + /** + * Verify payment after Razorpay checkout callback + */ + verify: async (params: { + razorpay_order_id: string + razorpay_payment_id: string + razorpay_signature: string + formId: string + fieldId: string + responseId?: string + }): Promise => { + const response = await fetch(`${API_URL}/payments/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + credentials: 'include', + }) + return handleResponse(response) + }, + + /** + * Get payment status by Razorpay order ID + */ + getStatus: async (orderId: string): Promise => { + const response = await fetch(`${API_URL}/payments/status/${orderId}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + }) + return handleResponse(response) + }, + + /** + * Get Razorpay key ID for frontend checkout + */ + getConfig: async (): Promise => { + const response = await fetch(`${API_URL}/payments/config`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + }) + return handleResponse(response) + }, +} diff --git a/src/components/field-properties.tsx b/src/components/field-properties.tsx index 38d19e9e..2281e912 100644 --- a/src/components/field-properties.tsx +++ b/src/components/field-properties.tsx @@ -67,6 +67,19 @@ export function FieldProperties({ const [validation, setValidation] = useState({}) const [showValidation, setShowValidation] = useState(false) + // Payment-specific state + const [paymentAmount, setPaymentAmount] = useState(0) + const [paymentCurrency, setPaymentCurrency] = useState('INR') + const [paymentDescription, setPaymentDescription] = useState('') + const [paymentBusinessName, setPaymentBusinessName] = useState('') + const [paymentLogoUrl, setPaymentLogoUrl] = useState('') + const [paymentThemeColor, setPaymentThemeColor] = useState('#6366f1') + const [paymentPrefillName, setPaymentPrefillName] = useState('') + const [paymentPrefillEmail, setPaymentPrefillEmail] = useState('') + const [paymentPrefillContact, setPaymentPrefillContact] = useState('') + const [paymentReceiptPrefix, setPaymentReceiptPrefix] = useState('receipt') + const [paymentNotes, setPaymentNotes] = useState('') + // Initialize local state when field prop changes // This syncs dialog with the field being edited useEffect(() => { @@ -78,9 +91,23 @@ export function FieldProperties({ setMin(field.min) setMax(field.max) setStep(field.step) - setOptionsString(field.options ? field.options.join('\n') : '') + setOptionsString(Array.isArray(field.options) ? field.options.join('\n') : '') setValidation(field.validation || {}) setShowValidation(false) + + // Initialize payment-specific state + const fieldOptions = field.options as { amount?: number; currency?: string; description?: string; businessName?: string; logoUrl?: string; themeColor?: string; prefillName?: string; prefillEmail?: string; prefillContact?: string; receiptPrefix?: string; notes?: string } | null + setPaymentAmount(fieldOptions?.amount || 0) + setPaymentCurrency(fieldOptions?.currency || 'INR') + setPaymentDescription(fieldOptions?.description || '') + setPaymentBusinessName(fieldOptions?.businessName || '') + setPaymentLogoUrl(fieldOptions?.logoUrl || '') + setPaymentThemeColor(fieldOptions?.themeColor || '#6366f1') + setPaymentPrefillName(fieldOptions?.prefillName || '') + setPaymentPrefillEmail(fieldOptions?.prefillEmail || '') + setPaymentPrefillContact(fieldOptions?.prefillContact || '') + setPaymentReceiptPrefix(fieldOptions?.receiptPrefix || 'receipt') + setPaymentNotes(fieldOptions?.notes || '') } }, [field]) @@ -103,6 +130,27 @@ export function FieldProperties({ } } + // Build options based on field type + let finalOptions: string[] | { amount: number; currency: string; description: string; businessName: string; logoUrl: string; themeColor: string; prefillName: string; prefillEmail: string; prefillContact: string; receiptPrefix: string; notes: string } | undefined + + if (field.type === 'payment') { + finalOptions = { + amount: paymentAmount, + currency: paymentCurrency, + description: paymentDescription, + businessName: paymentBusinessName, + logoUrl: paymentLogoUrl, + themeColor: paymentThemeColor, + prefillName: paymentPrefillName, + prefillEmail: paymentPrefillEmail, + prefillContact: paymentPrefillContact, + receiptPrefix: paymentReceiptPrefix, + notes: paymentNotes, + } + } else if (optionsString) { + finalOptions = optionsString.split('\n').filter((s) => s.trim() !== '') + } + onSave({ ...field, label, @@ -111,9 +159,7 @@ export function FieldProperties({ min: min !== undefined && !isNaN(min) ? Number(min) : undefined, max: max !== undefined && !isNaN(max) ? Number(max) : undefined, step: step !== undefined && !isNaN(step) ? Number(step) : undefined, - options: optionsString - ? optionsString.split('\n').filter((s) => s.trim() !== '') - : undefined, + options: finalOptions, validation: finalValidation, }) onOpenChange(false) @@ -149,16 +195,16 @@ export function FieldProperties({ {['text', 'textarea', 'email', 'url', 'phone', 'number'].includes( field.type, ) && ( - - Placeholder - setPlaceholder(e.target.value)} - placeholder="Placeholder text" - /> - - )} + + Placeholder + setPlaceholder(e.target.value)} + placeholder="Placeholder text" + /> + + )} {/* Min/Max/Step for number and slider */} {['number', 'slider'].includes(field.type) && ( @@ -227,6 +273,171 @@ export function FieldProperties({ )} + {/* Payment-specific options */} + {field.type === 'payment' && ( +
+ {/* ── Payment Details ── */} +
+

Payment Details

+
+ + Amount + + setPaymentAmount( + e.target.value === '' ? 0 : Number(e.target.value), + ) + } + placeholder="0.00" + /> + + + Currency + + +
+ + Payment Description + setPaymentDescription(e.target.value)} + placeholder="e.g. Application fee, Course enrollment" + /> + Shown to the payer in the checkout modal. + +
+ + {/* ── Merchant Branding ── */} +
+

Merchant Branding

+ + Business / Merchant Name + setPaymentBusinessName(e.target.value)} + placeholder="e.g. Acme Corp" + /> + Displayed at the top of the Razorpay checkout modal. + + + Logo URL (optional) + setPaymentLogoUrl(e.target.value)} + placeholder="https://yourdomain.com/logo.png" + /> + Your brand logo shown in the payment modal. + + + Theme Color +
+ setPaymentThemeColor(e.target.value)} + className="h-9 w-12 cursor-pointer rounded-md border border-input bg-background p-1" + /> + setPaymentThemeColor(e.target.value)} + placeholder="#6366f1" + className="font-mono" + /> +
+ Accent color for the Razorpay checkout UI. +
+
+ + {/* ── Prefill Respondent Info ── */} +
+

Prefill Customer Info

+ + These values pre-populate Razorpay's checkout form fields. Leave blank to let the payer fill them. + + + Name + setPaymentPrefillName(e.target.value)} + placeholder="e.g. John Doe" + /> + + + Email + setPaymentPrefillEmail(e.target.value)} + placeholder="e.g. john@example.com" + /> + + + Phone / Contact + setPaymentPrefillContact(e.target.value)} + placeholder="e.g. +919876543210" + /> + +
+ + {/* ── Receipt & Notes ── */} +
+

Receipt & Notes

+ + Receipt Prefix + setPaymentReceiptPrefix(e.target.value)} + placeholder="e.g. invoice, order, reg" + /> + + Prefixed to the auto-generated receipt ID (e.g. invoice_20240311_abc). + + + + Notes +