Added customer IP programatically

This commit is contained in:
Mitchell Magro 2026-01-13 15:23:25 +01:00
parent bf8c77f490
commit 52e219e258
6 changed files with 121 additions and 9 deletions

View File

@ -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<string, unknown>)[ipPath[1]] = customerIp;
} else {
// Other nested paths
let current: Record<string, unknown> = additionalFields;
for (let i = 0; i < ipPath.length - 1; i++) {
if (!current[ipPath[i]]) {
current[ipPath[i]] = {};
}
current = current[ipPath[i]] as Record<string, unknown>;
}
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<string, unknown>).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<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: config.userId || '',
first_name: '',
last_name: '',
email: '',
},
customer: customerData,
redirect: formData.redirect as IPaymentRequest['redirect'],
...additionalFields,
};

View File

@ -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<void>;
@ -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,

View File

@ -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<StateChangeListener> = 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<void> {
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) {

View File

@ -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;
}

View File

@ -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<IFormMetadataField[]> {
): 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(

28
src/utils/ipExtractor.ts Normal file
View File

@ -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;
}