Added dynamic forms

This commit is contained in:
Mitchell Magro 2025-12-30 18:27:24 +01:00
parent 4f90aeff3c
commit e57f170470
10 changed files with 430 additions and 236 deletions

View File

@ -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<IPaymentRequest, 'merchant_id'>) => {
const handleFormSubmit = async (formData: Record<string, unknown>) => {
try {
await initiatePayment(formData);
// Transform form data to match IPaymentRequest structure
const additionalFields: Record<string, unknown> = {};
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<IPaymentRequest, 'merchant_id'> = {
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 <Status type="error" message={`Error: ${error.message}`} />;
return <Status type="error" message={`Error: ${error.message}`} onAction={()=> goBack()}/>;
}
if (state === 'redirecting') {
@ -61,6 +93,7 @@ function Cashier() {
<PaymentForm
method={selectedMethod}
currencies={currencies}
formMetadata={formMetadata}
config={getCashierConfig()}
onSubmit={handleFormSubmit}
onBack={handleBack}

View File

@ -1,10 +1,18 @@
import { useState } from 'react';
import { useState, useMemo } from 'react';
import type { FormEvent } from 'react';
import { bem } from '@/utils/bem';
import type { IPaymentMethod } from '@/features/payment-methods/types';
import type { IApiCurrency, ICustomer, IRedirect } from '../types';
import type { IApiCurrency, IFormMetadataField, IRedirect } from '../types';
import type { ICashierConfig } from '@/config/cashierConfig';
import { getDefaultRedirectUrls, normalizeRedirectUrl } from '@/config/cashierConfig';
import {
parseFormFields,
shouldShowField,
groupFieldsBySection,
buildNestedObject,
mergeNestedObjects,
type IParsedField,
} from '../utils/formBuilder';
import './PaymentForm.scss';
const block = 'payment-form';
@ -12,16 +20,9 @@ const block = 'payment-form';
interface IPaymentFormProps {
method: IPaymentMethod;
currencies: IApiCurrency[];
formMetadata: IFormMetadataField[];
config: ICashierConfig;
onSubmit: (data: {
payment_type: 'deposit' | 'withdrawal';
method: string;
currency: string;
amount: number;
customer: ICustomer;
redirect: IRedirect;
account?: string;
}) => void;
onSubmit: (data: Record<string, unknown>) => 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<string>(config?.currency || currencies[0]?.code || '');
const [amount, setAmount] = useState<string>('');
const [account, setAccount] = useState<string>(config.account || '');
const [formData, setFormData] = useState<Record<string, unknown>>(() => {
const initial: Record<string, unknown> = {
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<string, unknown>)[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 (
<div key={field.code} className={bem(block, 'radio-group')}>
<label className={bem(block, 'radio')}>
<input
type="radio"
value="deposit"
checked={value === 'deposit'}
onChange={e => setFieldValue(field, e.target.value)}
/>
<span>Deposit</span>
</label>
<label className={bem(block, 'radio')}>
<input
type="radio"
value="withdrawal"
checked={value === 'withdrawal'}
onChange={e => setFieldValue(field, e.target.value)}
/>
<span>Withdrawal</span>
</label>
</div>
);
}
if (field.inputType === 'select' && field.code === 'currency') {
return (
<div key={field.code} className={bem(block, 'field')}>
<label className={bem(block, 'label')} htmlFor={fieldId}>
{field.name} <span className={bem(block, 'required')}>*</span>
</label>
<select
id={fieldId}
className={bem(block, 'input')}
value={value}
onChange={e => setFieldValue(field, e.target.value)}
required
>
<option value="">Select currency</option>
{currencies.map(curr => (
<option key={curr.code} value={curr.code}>
{curr.name} ({curr.code})
</option>
))}
</select>
</div>
);
}
const inputProps: Record<string, unknown> = {
id: fieldId,
className: bem(block, 'input'),
value,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => 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 (
<div key={field.code} className={bem(block, 'field')}>
<label className={bem(block, 'label')} htmlFor={fieldId}>
{field.name} <span className={bem(block, 'required')}>*</span>
</label>
<input {...inputProps} />
</div>
);
};
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<string, unknown> = {
...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 (
<div className={block}>
@ -101,87 +214,35 @@ function PaymentForm({
</div>
<form className={bem(block, 'form')} onSubmit={handleSubmit}>
{showPaymentType && (
{showPaymentType && groupedFields.payment.some(f => f.code === 'type') && (
<div className={bem(block, 'section')}>
<h3 className={bem(block, 'section-title')}>Payment Type</h3>
<div className={bem(block, 'radio-group')}>
<label className={bem(block, 'radio')}>
<input
type="radio"
value="deposit"
checked={paymentType === 'deposit'}
onChange={e => setPaymentType(e.target.value as 'deposit' | 'withdrawal')}
/>
<span>Deposit</span>
</label>
<label className={bem(block, 'radio')}>
<input
type="radio"
value="withdrawal"
checked={paymentType === 'withdrawal'}
onChange={e => setPaymentType(e.target.value as 'withdrawal')}
/>
<span>Withdrawal</span>
</label>
</div>
{groupedFields.payment.filter(f => f.code === 'type').map(renderField)}
</div>
)}
<div className={bem(block, 'section')}>
<h3 className={bem(block, 'section-title')}>Payment Information</h3>
{showCurrency && (
<div className={bem(block, 'field')}>
<label className={bem(block, 'label')}>
Currency <span className={bem(block, 'required')}>*</span>
</label>
<select
className={bem(block, 'input')}
value={currency}
onChange={e => setCurrency(e.target.value)}
required
>
{currencies.map(curr => (
<option key={curr.code} value={curr.code}>
{curr.name} ({curr.code})
</option>
))}
</select>
</div>
)}
<div className={bem(block, 'field')}>
<label className={bem(block, 'label')}>
Amount <span className={bem(block, 'required')}>*</span>
</label>
<input
type="number"
className={bem(block, 'input')}
value={amount}
onChange={e => 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 && (
<div className={bem(block, 'section')}>
<h3 className={bem(block, 'section-title')}>Payment Information</h3>
{groupedFields.payment
.filter(f => f.code !== 'type' && f.code !== 'method')
.map(renderField)}
</div>
)}
{showAccount && (
<div className={bem(block, 'field')}>
<label className={bem(block, 'label')}>
Account (IBAN) <span className={bem(block, 'required')}>*</span>
</label>
<input
type="text"
className={bem(block, 'input')}
value={account}
onChange={e => setAccount(e.target.value)}
required
placeholder="Enter IBAN or account number"
/>
</div>
)}
</div>
{groupedFields.customer.length > 0 && (
<div className={bem(block, 'section')}>
<h3 className={bem(block, 'section-title')}>Customer Information</h3>
{groupedFields.customer.map(renderField)}
</div>
)}
{groupedFields.account.length > 0 && (
<div className={bem(block, 'section')}>
<h3 className={bem(block, 'section-title')}>Account Information</h3>
{groupedFields.account.map(renderField)}
</div>
)}
<div className={bem(block, 'actions')}>
<button type="button" className={bem(block, 'button', 'secondary')} onClick={onBack}>

View File

@ -1,7 +1,12 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import { cashierService } from '../services/CashierService';
import type { TCashierFlowState, IApiCurrency, IPaymentRequest } from '../types';
import type {
TCashierFlowState,
IApiCurrency,
IPaymentRequest,
IFormMetadataField,
} from '../types';
import type { IPaymentMethod } from '@/features/payment-methods/types';
import { DEFAULT_MERCHANT, type TMerchant } from '@/config/api';
@ -10,10 +15,11 @@ interface ICashierContextValue {
methods: IPaymentMethod[];
currencies: IApiCurrency[];
selectedMethod: IPaymentMethod | null;
formMetadata: IFormMetadataField[];
error: Error | null;
paymentUrl: string | null;
loadData: (merchant?: TMerchant) => Promise<void>;
setSelectedMethod: (method: IPaymentMethod | null) => void;
setSelectedMethod: (method: IPaymentMethod | null, merchant?: TMerchant) => Promise<void>;
initiatePayment: (
paymentRequest: Omit<IPaymentRequest, 'merchant_id'>,
merchant?: TMerchant
@ -33,6 +39,7 @@ interface ICashierState {
methods: IPaymentMethod[];
currencies: IApiCurrency[];
selectedMethod: IPaymentMethod | null;
formMetadata: IFormMetadataField[];
error: Error | null;
paymentUrl: string | null;
}
@ -43,6 +50,7 @@ export function CashierProvider({ children }: ICashierProviderProps) {
methods: cashierService.getMethods(),
currencies: cashierService.getCurrencies(),
selectedMethod: cashierService.getSelectedMethod(),
formMetadata: cashierService.getFormMetadata(),
error: cashierService.getError(),
paymentUrl: cashierService.getPaymentUrl(),
}));
@ -54,6 +62,7 @@ export function CashierProvider({ children }: ICashierProviderProps) {
methods: cashierService.getMethods(),
currencies: cashierService.getCurrencies(),
selectedMethod: cashierService.getSelectedMethod(),
formMetadata: cashierService.getFormMetadata(),
error: cashierService.getError(),
paymentUrl: cashierService.getPaymentUrl(),
});
@ -78,8 +87,11 @@ export function CashierProvider({ children }: ICashierProviderProps) {
await cashierService.initiatePayment(paymentRequest, merchant);
};
const setSelectedMethod = (method: IPaymentMethod | null) => {
cashierService.setSelectedMethod(method);
const setSelectedMethod = async (
method: IPaymentMethod | null,
merchant: TMerchant = DEFAULT_MERCHANT
) => {
await cashierService.setSelectedMethod(method, merchant);
};
const goBack = () => {
@ -95,6 +107,7 @@ export function CashierProvider({ children }: ICashierProviderProps) {
methods: cashierState.methods,
currencies: cashierState.currencies,
selectedMethod: cashierState.selectedMethod,
formMetadata: cashierState.formMetadata,
error: cashierState.error,
paymentUrl: cashierState.paymentUrl,
loadData,

View File

@ -1,29 +0,0 @@
import { useState, useEffect } from 'react';
import { apiService } from '@/services/api';
import type { IApiCurrency } from '../types';
import type { TMerchant } from '@/config/api';
export function useCurrencies(merchant?: TMerchant) {
const [currencies, setCurrencies] = useState<IApiCurrency[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchCurrencies = async () => {
try {
setLoading(true);
setError(null);
const data = await apiService.getCurrencies(merchant);
setCurrencies(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch currencies'));
} finally {
setLoading(false);
}
};
fetchCurrencies();
}, [merchant]);
return { currencies, loading, error };
}

View File

@ -1,43 +0,0 @@
import { useState } from 'react';
import { apiService } from '@/services/api';
import { MERCHANT_IDS, DEFAULT_MERCHANT, type TMerchant } from '@/config/api';
import type { IPaymentRequest, IPaymentResponse } from '../types';
export function usePayment() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [paymentUrl, setPaymentUrl] = useState<string | null>(null);
const initiatePayment = async (
paymentRequest: Omit<IPaymentRequest, 'merchant_id'>,
merchant: TMerchant = DEFAULT_MERCHANT
) => {
try {
setLoading(true);
setError(null);
setPaymentUrl(null);
const fullRequest: IPaymentRequest = {
...paymentRequest,
merchant_id: MERCHANT_IDS[merchant],
};
const response = await apiService.initiatePayment(fullRequest, merchant);
setPaymentUrl(response.payment_url);
return response;
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to initiate payment');
setError(error);
throw error;
} finally {
setLoading(false);
}
};
return {
initiatePayment,
loading,
error,
paymentUrl,
};
}

View File

@ -1,29 +0,0 @@
import { useState, useEffect } from 'react';
import { apiService } from '@/services/api';
import type { TMerchant } from '@/config/api';
import type { IPaymentMethod } from '@/features/payment-methods/types';
export function usePaymentMethods(merchant?: TMerchant) {
const [methods, setMethods] = useState<IPaymentMethod[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchMethods = async () => {
try {
setLoading(true);
setError(null);
const data = await apiService.getMethods(merchant);
setMethods(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch payment methods'));
} finally {
setLoading(false);
}
};
fetchMethods();
}, [merchant]);
return { methods, loading, error };
}

View File

@ -1,7 +1,13 @@
import { apiService } from '@/services/api';
import { DEFAULT_MERCHANT, MERCHANT_IDS, type TMerchant } from '@/config/api';
import type { TCashierFlowState, IApiCurrency, IPaymentRequest } from '../types';
import type {
TCashierFlowState,
IApiCurrency,
IPaymentRequest,
IFormMetadataField,
} from '../types';
import type { IPaymentMethod } from '@/features/payment-methods/types';
import { getCashierConfig } from '@/config/cashierConfig';
type StateChangeListener = (state: TCashierFlowState) => void;
@ -10,6 +16,7 @@ class CashierService {
private methods: IPaymentMethod[] = [];
private currencies: IApiCurrency[] = [];
private selectedMethod: IPaymentMethod | null = null;
private formMetadata: IFormMetadataField[] = [];
private error: Error | null = null;
private paymentUrl: string | null = null;
private listeners: Set<StateChangeListener> = new Set();
@ -38,8 +45,29 @@ class CashierService {
return this.selectedMethod;
}
setSelectedMethod(method: IPaymentMethod | null): void {
getFormMetadata(): IFormMetadataField[] {
return this.formMetadata;
}
async setSelectedMethod(
method: IPaymentMethod | null,
merchant: TMerchant = DEFAULT_MERCHANT
): Promise<void> {
this.selectedMethod = method;
this.formMetadata = [];
if (method) {
try {
const config = getCashierConfig();
const paymentType = config.paymentType || 'deposit';
this.formMetadata = await apiService.getFormMetadata(method.code, paymentType, merchant);
} catch (err) {
// If form metadata fetch fails, we still allow method selection
// Error can be handled by the UI if needed
console.error('Failed to fetch form metadata:', err);
}
}
this.notifyListeners();
}
@ -89,14 +117,14 @@ class CashierService {
this.setState('submitting');
this.error = null;
const fullRequest: IPaymentRequest = {
const fullRequest = {
...paymentRequest,
merchant_id: MERCHANT_IDS[merchant],
};
} as IPaymentRequest;
const response = await apiService.initiatePayment(fullRequest, merchant);
this.paymentUrl = response.payment_url;
this.paymentUrl = response.redirect_url;
this.setState('redirecting');
} catch (err) {
this.error = err instanceof Error ? err : new Error('Failed to initiate payment');
@ -108,6 +136,7 @@ class CashierService {
reset(): void {
this.state = 'loading';
this.selectedMethod = null;
this.formMetadata = [];
this.error = null;
this.paymentUrl = null;
this.notifyListeners();
@ -115,7 +144,14 @@ class CashierService {
goBack(): void {
this.selectedMethod = null;
this.notifyListeners();
this.formMetadata = [];
this.error = null;
// Reset to ready state if we have methods available, otherwise keep current state
if (this.methods.length > 0) {
this.setState('ready');
} else {
this.notifyListeners();
}
}
}

View File

@ -23,6 +23,7 @@ export interface ICustomer {
first_name: string;
last_name: string;
email: string;
[key: string]: unknown; // Allow additional customer fields
}
export interface IRedirect {
@ -40,10 +41,11 @@ export interface IPaymentRequest {
amount: number; // float64
redirect: IRedirect;
account?: string; // Required for withdrawals (IBAN or account number)
[key: string]: unknown; // Allow additional fields
}
export interface IPaymentResponse {
payment_url: string; // URL where customer needs to continue with payment flow
redirect_url: string; // URL where customer needs to continue with payment flow
}
export type TCashierResultStatus = 'success' | 'cancel' | 'error';
@ -53,3 +55,13 @@ export interface ICashierResultMessage {
status: TCashierResultStatus;
message?: string;
}
export interface IFormMetadataField {
code: string;
name: string;
sort: number;
}
export interface IFormMetadataResponse {
data: IFormMetadataField[];
}

View File

@ -0,0 +1,114 @@
import type { IFormMetadataField } from '../types';
export interface IParsedField extends IFormMetadataField {
path: string[];
isNested: boolean;
inputType: string;
}
export function parseFieldCode(code: string): string[] {
return code.split('.');
}
export function getInputType(code: string): string {
const lowerCode = code.toLowerCase();
if (lowerCode.includes('email')) return 'email';
if (lowerCode.includes('phone')) return 'tel';
if (lowerCode.includes('birthdate') || lowerCode.includes('date')) return 'date';
if (lowerCode.includes('amount')) return 'number';
if (lowerCode.includes('zipcode') || lowerCode.includes('zip')) return 'text';
if (lowerCode === 'type') return 'radio';
if (lowerCode === 'method') return 'select';
if (lowerCode === 'currency') return 'select';
return 'text';
}
export function parseFormFields(fields: IFormMetadataField[]): IParsedField[] {
return fields
?.map(field => ({
...field,
path: parseFieldCode(field.code),
isNested: field.code.includes('.'),
inputType: getInputType(field.code),
}))
.sort((a, b) => a.sort - b.sort);
}
export function shouldShowField(
field: IParsedField,
config: { paymentType?: 'deposit' | 'withdrawal'; currency?: string }
): boolean {
const code = field.code.toLowerCase();
// Always show type if not in config
if (code === 'type' && !config.paymentType) return true;
if (code === 'type' && config.paymentType) return false;
// Always show method (but we might not render it as it's already selected)
if (code === 'method') return false; // Method is already selected, don't show in form
// Show currency if not in config
if (code === 'currency' && !config.currency) return true;
if (code === 'currency' && config.currency) return false;
// Show all other fields
return true;
}
export function buildNestedObject(path: string[], value: unknown): Record<string, unknown> {
const result: Record<string, unknown> = {};
let current = result;
for (let i = 0; i < path.length - 1; i++) {
current[path[i]] = {};
current = current[path[i]] as Record<string, unknown>;
}
current[path[path.length - 1]] = value;
return result;
}
export function mergeNestedObjects(
obj1: Record<string, unknown>,
obj2: Record<string, unknown>
): Record<string, unknown> {
const result = { ...obj1 };
for (const key in obj2) {
if (typeof obj2[key] === 'object' && obj2[key] !== null && !Array.isArray(obj2[key])) {
result[key] = mergeNestedObjects(
(result[key] as Record<string, unknown>) || {},
obj2[key] as Record<string, unknown>
);
} else {
result[key] = obj2[key];
}
}
return result;
}
export function groupFieldsBySection(fields: IParsedField[]): {
payment: IParsedField[];
customer: IParsedField[];
account: IParsedField[];
} {
const payment: IParsedField[] = [];
const customer: IParsedField[] = [];
const account: IParsedField[] = [];
fields?.forEach(field => {
if (field.path[0] === 'customer' && field.path[1] === 'account') {
account.push(field);
} else if (field.path[0] === 'customer') {
customer.push(field);
} else {
payment.push(field);
}
});
return { payment, customer, account };
}

View File

@ -5,6 +5,8 @@ import type {
IApiCurrenciesResponse,
IPaymentRequest,
IPaymentResponse,
IFormMetadataResponse,
IFormMetadataField,
} from '@/features/cashier/types';
import { mapApiMethodsToPaymentMethods } from '@/features/cashier/utils/methodMapper';
import type { IPaymentMethod } from '@/features/payment-methods/types';
@ -58,6 +60,29 @@ class ApiService {
return data.data;
}
async getFormMetadata(
method: string,
type: 'deposit' | 'withdrawal',
merchant: TMerchant = DEFAULT_MERCHANT
): Promise<IFormMetadataField[]> {
const params = new URLSearchParams({
method,
type,
});
const response = await fetch(`${API_BASE_URL}/form?${params.toString()}`, {
method: 'GET',
headers: this.getAuthHeader(merchant),
});
if (!response.ok) {
throw new Error(`Failed to fetch form metadata: ${response.statusText}`);
}
const data: IFormMetadataResponse = await response.json();
return data.data;
}
async initiatePayment(
paymentRequest: IPaymentRequest,
merchant: TMerchant = DEFAULT_MERCHANT
@ -69,7 +94,8 @@ class ApiService {
});
if (!response.ok) {
throw new Error(`Failed to initiate payment: ${response.statusText}`);
const data = await response.json();
throw new Error(`Failed to initiate payment: ${response.statusText} - ${data?.error?.detail}`);
}
return response.json();