diff --git a/src/features/cashier/Cashier.tsx b/src/features/cashier/Cashier.tsx index d14ab1e..31356ec 100644 --- a/src/features/cashier/Cashier.tsx +++ b/src/features/cashier/Cashier.tsx @@ -13,6 +13,7 @@ function Cashier() { currencies, selectedMethod, formMetadata, + customerIp, setSelectedMethod, initiatePayment, } = useCashier(); @@ -35,6 +36,50 @@ function Cashier() { additionalFields.account = formData.account as string; } + // Auto-fill customer IP if available and not already set + if (customerIp) { + // Check if IP field exists in form metadata and populate it + const ipField = formMetadata.find( + field => + field.code.toLowerCase().includes('ip') || + field.code === 'customer.ip' || + field.code === 'customer_ip' || + field.code === 'ip_address' + ); + + if (ipField) { + // Build nested path for IP field + const ipPath = ipField.code.split('.'); + if (ipPath.length === 1) { + // Top-level field like 'customer_ip' or 'ip_address' + additionalFields[ipField.code] = customerIp; + } else if (ipPath[0] === 'customer') { + // Nested field like 'customer.ip' + if (!additionalFields.customer) { + additionalFields.customer = {}; + } + (additionalFields.customer as Record)[ipPath[1]] = customerIp; + } else { + // Other nested paths + let current: Record = additionalFields; + for (let i = 0; i < ipPath.length - 1; i++) { + if (!current[ipPath[i]]) { + current[ipPath[i]] = {}; + } + current = current[ipPath[i]] as Record; + } + current[ipPath[ipPath.length - 1]] = customerIp; + } + } else { + // If no IP field found in metadata, try common field names + // Try customer.ip first (most common) + if (!additionalFields.customer) { + additionalFields.customer = {}; + } + (additionalFields.customer as Record).ip = customerIp; + } + } + // Preserve any additional fields Object.entries(formData).forEach(([key, value]) => { if ( @@ -52,18 +97,25 @@ function Cashier() { } }); + // Merge customer data from formData with customer data in additionalFields + const customerData = (formData.customer as IPaymentRequest['customer']) || { + id: config.userId || '', + first_name: '', + last_name: '', + email: '', + }; + + if (additionalFields.customer) { + Object.assign(customerData, additionalFields.customer); + delete additionalFields.customer; + } 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: config.userId || '', - first_name: '', - last_name: '', - email: '', - }, + customer: customerData, redirect: formData.redirect as IPaymentRequest['redirect'], ...additionalFields, }; diff --git a/src/features/cashier/context/CashierContext.tsx b/src/features/cashier/context/CashierContext.tsx index 8f8abae..e883879 100644 --- a/src/features/cashier/context/CashierContext.tsx +++ b/src/features/cashier/context/CashierContext.tsx @@ -16,6 +16,7 @@ interface ICashierContextValue { currencies: IApiCurrency[]; selectedMethod: IPaymentMethod | null; formMetadata: IFormMetadataField[]; + customerIp: string | null; error: Error | null; paymentUrl: string | null; loadData: (merchant?: TMerchant) => Promise; @@ -40,6 +41,7 @@ interface ICashierState { currencies: IApiCurrency[]; selectedMethod: IPaymentMethod | null; formMetadata: IFormMetadataField[]; + customerIp: string | null; error: Error | null; paymentUrl: string | null; } @@ -51,6 +53,7 @@ export function CashierProvider({ children }: ICashierProviderProps) { currencies: cashierService.getCurrencies(), selectedMethod: cashierService.getSelectedMethod(), formMetadata: cashierService.getFormMetadata(), + customerIp: cashierService.getCustomerIp(), error: cashierService.getError(), paymentUrl: cashierService.getPaymentUrl(), })); @@ -63,6 +66,7 @@ export function CashierProvider({ children }: ICashierProviderProps) { currencies: cashierService.getCurrencies(), selectedMethod: cashierService.getSelectedMethod(), formMetadata: cashierService.getFormMetadata(), + customerIp: cashierService.getCustomerIp(), error: cashierService.getError(), paymentUrl: cashierService.getPaymentUrl(), }); @@ -106,6 +110,7 @@ export function CashierProvider({ children }: ICashierProviderProps) { currencies: cashierState.currencies, selectedMethod: cashierState.selectedMethod, formMetadata: cashierState.formMetadata, + customerIp: cashierState.customerIp, error: cashierState.error, paymentUrl: cashierState.paymentUrl, loadData, diff --git a/src/features/cashier/services/CashierService.ts b/src/features/cashier/services/CashierService.ts index fb0a08a..842d219 100644 --- a/src/features/cashier/services/CashierService.ts +++ b/src/features/cashier/services/CashierService.ts @@ -17,6 +17,7 @@ class CashierService { private currencies: IApiCurrency[] = []; private selectedMethod: IPaymentMethod | null = null; private formMetadata: IFormMetadataField[] = []; + private customerIp: string | null = null; private error: Error | null = null; private paymentUrl: string | null = null; private listeners: Set = new Set(); @@ -49,12 +50,17 @@ class CashierService { return this.formMetadata; } + getCustomerIp(): string | null { + return this.customerIp; + } + async setSelectedMethod( method: IPaymentMethod | null, merchant: TMerchant = DEFAULT_MERCHANT ): Promise { this.selectedMethod = method; this.formMetadata = []; + this.customerIp = null; if (method) { try { @@ -68,7 +74,8 @@ class CashierService { apiService.getCurrencies(merchant, method.code), ]); - this.formMetadata = formMetadataData; + this.formMetadata = formMetadataData.fields; + this.customerIp = formMetadataData.customerIp; this.currencies = currenciesData; } catch (err) { // If form metadata or currencies fetch fails, we still allow method selection @@ -149,6 +156,7 @@ class CashierService { this.selectedMethod = null; this.formMetadata = []; this.currencies = []; + this.customerIp = null; this.error = null; this.paymentUrl = null; this.notifyListeners(); @@ -158,6 +166,7 @@ class CashierService { this.selectedMethod = null; this.formMetadata = []; this.currencies = []; + this.customerIp = null; this.error = null; // Reset to ready state if we have methods available, otherwise keep current state if (this.methods.length > 0) { diff --git a/src/features/cashier/utils/formBuilder.ts b/src/features/cashier/utils/formBuilder.ts index 31c0c5e..4e86aa5 100644 --- a/src/features/cashier/utils/formBuilder.ts +++ b/src/features/cashier/utils/formBuilder.ts @@ -58,6 +58,18 @@ export function shouldShowField( return false; } + // Hide customer IP fields - they're set programmatically from API response headers + const lowerCode = code.toLowerCase(); + if ( + lowerCode.includes('ip') || + lowerCode === 'customer.ip' || + lowerCode === 'customer_ip' || + lowerCode === 'ip_address' || + (field.path[0] === 'customer' && field.path[1]?.toLowerCase() === 'ip') + ) { + return false; + } + // Show all other fields return true; } diff --git a/src/services/api.ts b/src/services/api.ts index 976aa70..4cd8d1f 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -10,6 +10,7 @@ import type { } from '@/features/cashier/types'; import { mapApiMethodsToPaymentMethods } from '@/features/cashier/utils/methodMapper'; import type { IPaymentMethod } from '@/features/payment-methods/types'; +import { extractIpFromHeaders } from '@/utils/ipExtractor'; class ApiService { private methods: IPaymentMethod[] = []; @@ -82,7 +83,7 @@ class ApiService { method: string, type: 'deposit' | 'withdrawal', merchant: TMerchant = DEFAULT_MERCHANT - ): Promise { + ): Promise<{ fields: IFormMetadataField[]; customerIp: string | null }> { const params = new URLSearchParams({ method, type, @@ -99,7 +100,12 @@ class ApiService { } const data: IFormMetadataResponse = await response.json(); - return data.data; + const customerIp = extractIpFromHeaders(response.headers); + + return { + fields: data.data, + customerIp, + }; } async initiatePayment( diff --git a/src/utils/ipExtractor.ts b/src/utils/ipExtractor.ts new file mode 100644 index 0000000..4501261 --- /dev/null +++ b/src/utils/ipExtractor.ts @@ -0,0 +1,28 @@ +/** + * Extracts the client IP address from response headers. + * Checks headers in order: CF-Connecting-IP, X-Forwarded-For, X-Real-IP + * @param headers - Response headers object + * @returns IP address string or null if not found + */ +export function extractIpFromHeaders(headers: Headers): string | null { + // Check Cloudflare header first (most reliable when behind Cloudflare) + const cfIp = headers.get('CF-Connecting-IP'); + if (cfIp) { + // X-Forwarded-For can contain multiple IPs, take the first one + return cfIp.split(',')[0].trim(); + } + + // Check X-Forwarded-For (can contain multiple IPs, first is usually the original client) + const forwardedFor = headers.get('X-Forwarded-For'); + if (forwardedFor) { + return forwardedFor.split(',')[0].trim(); + } + + // Check X-Real-IP (nginx proxy header) + const realIp = headers.get('X-Real-IP'); + if (realIp) { + return realIp.split(',')[0].trim(); + } + + return null; +}