Added Method currency fetching

This commit is contained in:
Mitchell Magro 2026-01-02 10:50:55 +01:00
parent e57f170470
commit 2de17b8675
12 changed files with 196 additions and 48 deletions

View File

@ -0,0 +1,24 @@
interface IApprovedIconProps {
size?: number;
}
function ApprovedIcon({ size = 20 }: IApprovedIconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Approved"
>
<circle cx="12" cy="12" r="10" />
<path d="M9 12l2 2 4-4" />
</svg>
);
}
export default ApprovedIcon;

View File

@ -0,0 +1,25 @@
interface ICancelledIconProps {
size?: number;
}
function CancelledIcon({ size = 20 }: ICancelledIconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Cancelled"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
);
}
export default CancelledIcon;

View File

@ -0,0 +1,25 @@
interface IErrorIconProps {
size?: number;
}
function ErrorIcon({ size = 20 }: IErrorIconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Error"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="7" x2="12" y2="13" />
<circle cx="12" cy="17" r="1" fill="currentColor" stroke="none" />
</svg>
);
}
export default ErrorIcon;

View File

@ -23,3 +23,22 @@
.status--cancel .status__message {
color: #f57c00;
}
.status__icon {
display: flex;
align-items: center;
justify-content: center;
color: inherit;
}
.status--cancel .status__icon {
color: #f57c00;
}
.status--success .status__icon {
color: #2e7d32;
}
.status--error .status__icon {
color: #d32f2f;
}

View File

@ -1,5 +1,8 @@
import './Status.scss';
import Button from '../Button/Button';
import CancelledIcon from '../Icons/CancelledIcon';
import ApprovedIcon from '../Icons/ApprovedIcon';
import ErrorIcon from '../Icons/ErrorIcon';
type TStatusType = 'error' | 'success' | 'cancel';
@ -18,6 +21,21 @@ function Status({ type, message, onAction, actionLabel }: IStatusProps) {
return (
<div className={`status status--${type}`}>
{type === 'cancel' && (
<div className="status__icon">
<CancelledIcon size={48} />
</div>
)}
{type === 'success' && (
<div className="status__icon">
<ApprovedIcon size={48} />
</div>
)}
{type === 'error' && (
<div className="status__icon">
<ErrorIcon size={48} />
</div>
)}
<p className="status__message">{message}</p>
{onAction && <Button onClick={onAction} label={getDefaultActionLabel()} />}
</div>

View File

@ -8,7 +8,6 @@ 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 {
@ -46,7 +45,15 @@ function Cashier() {
// Preserve any additional fields
Object.entries(formData).forEach(([key, value]) => {
if (
!['payment_type', 'method', 'currency', 'amount', 'customer', 'redirect', 'account'].includes(key)
![
'payment_type',
'method',
'currency',
'amount',
'customer',
'redirect',
'account',
].includes(key)
) {
additionalFields[key] = value;
}
@ -81,7 +88,7 @@ function Cashier() {
}
if (state === 'error' && error) {
return <Status type="error" message={`Error: ${error.message}`} onAction={()=> goBack()}/>;
return <Status type="error" message={`Error: ${error.message}`} onAction={() => goBack()} />;
}
if (state === 'redirecting') {
@ -91,6 +98,7 @@ function Cashier() {
if (selectedMethod) {
return (
<PaymentForm
key={selectedMethod.code}
method={selectedMethod}
currencies={currencies}
formMetadata={formMetadata}

View File

@ -40,7 +40,7 @@ function PaymentForm({
const initial: Record<string, unknown> = {
type: config?.paymentType || 'deposit',
method: method.code,
currency: config?.currency || currencies[0]?.code || '',
currency: currencies.length > 0 ? currencies[0]?.code : '',
amount: '',
};
@ -52,8 +52,9 @@ function PaymentForm({
return initial;
});
console.log('currencies', currencies);
const parsedFields = useMemo(() => {
if (formMetadata?. length === 0) return [];
if (formMetadata?.length === 0) return [];
return parseFormFields(formMetadata);
}, [formMetadata]);

View File

@ -69,9 +69,7 @@ export function CashierProvider({ children }: ICashierProviderProps) {
});
// Initial load
cashierService.loadData(DEFAULT_MERCHANT).catch(() => {
// Error is handled by state
});
cashierService.loadData(DEFAULT_MERCHANT);
return unsubscribe;
}, []);

View File

@ -60,12 +60,24 @@ class CashierService {
try {
const config = getCashierConfig();
const paymentType = config.paymentType || 'deposit';
this.formMetadata = await apiService.getFormMetadata(method.code, paymentType, merchant);
// Fetch form metadata and currencies for the selected method in parallel
const [formMetadataData, currenciesData] = await Promise.all([
apiService.getFormMetadata(method.code, paymentType, merchant),
apiService.getCurrencies(merchant, method.code),
]);
this.formMetadata = formMetadataData;
this.currencies = currenciesData;
} catch (err) {
// If form metadata fetch fails, we still allow method selection
// If form metadata or currencies fetch fails, we still allow method selection
// Error can be handled by the UI if needed
console.error('Failed to fetch form metadata:', err);
console.error('Failed to fetch form metadata or currencies:', err);
}
} else {
// Reset currencies when method is deselected
// Optionally reload all currencies or keep empty
this.currencies = [];
}
this.notifyListeners();
@ -94,13 +106,11 @@ class CashierService {
this.setState('loading');
this.error = null;
const [methodsData, currenciesData] = await Promise.all([
apiService.getMethods(merchant),
apiService.getCurrencies(merchant),
]);
// Only fetch methods, currencies will be fetched when a method is selected
const methodsData = await apiService.getMethods(merchant);
this.methods = methodsData;
this.currencies = currenciesData;
this.currencies = []; // Currencies will be loaded when method is selected
this.setState('ready');
} catch (err) {
this.error = err instanceof Error ? err : new Error('Failed to load cashier data');
@ -137,6 +147,7 @@ class CashierService {
this.state = 'loading';
this.selectedMethod = null;
this.formMetadata = [];
this.currencies = [];
this.error = null;
this.paymentUrl = null;
this.notifyListeners();
@ -145,6 +156,7 @@ class CashierService {
goBack(): void {
this.selectedMethod = null;
this.formMetadata = [];
this.currencies = [];
this.error = null;
// Reset to ready state if we have methods available, otherwise keep current state
if (this.methods.length > 0) {

View File

@ -111,4 +111,3 @@ export function groupFieldsBySection(fields: IParsedField[]): {
return { payment, customer, account };
}

View File

@ -37,6 +37,18 @@ function PaymentStatus() {
return 'Close';
};
function getDefaultMessage(type: TCashierResultStatus): string {
switch (type) {
case 'success':
return 'Payment completed successfully!';
case 'cancel':
return 'Payment was cancelled.';
case 'error':
return 'An error occurred during payment processing.';
default:
return 'Payment status unknown.';
}
}
return (
<div className="payment-status">
<Status
@ -49,17 +61,4 @@ function PaymentStatus() {
);
}
function getDefaultMessage(type: TCashierResultStatus): string {
switch (type) {
case 'success':
return 'Payment completed successfully!';
case 'cancel':
return 'Payment was cancelled.';
case 'error':
return 'An error occurred during payment processing.';
default:
return 'Payment status unknown.';
}
}
export default PaymentStatus;

View File

@ -13,7 +13,7 @@ import type { IPaymentMethod } from '@/features/payment-methods/types';
class ApiService {
private methods: IPaymentMethod[] = [];
private currencies: IApiCurrency[] = [];
private currencies: IApiCurrency[] = []; // Cache for currencies without method filter
private getAuthHeader(merchant: TMerchant = DEFAULT_MERCHANT): HeadersInit {
const apiKey = MERCHANT_API_KEYS[merchant];
@ -43,11 +43,23 @@ class ApiService {
return this.methods;
}
async getCurrencies(merchant: TMerchant = DEFAULT_MERCHANT): Promise<IApiCurrency[]> {
if (this.currencies.length > 0) {
async getCurrencies(
merchant: TMerchant = DEFAULT_MERCHANT,
method?: string
): Promise<IApiCurrency[]> {
// If method is provided, always fetch fresh data (don't use cache)
// If no method and we have cached currencies, return cached
if (!method && this.currencies.length > 0) {
return this.currencies;
}
const response = await fetch(`${API_BASE_URL}/currencies`, {
let url = `${API_BASE_URL}/currencies`;
if (method) {
const separator = url.includes('?') ? '&' : '?';
url = `${url}${separator}method=${encodeURIComponent(method)}`;
}
const response = await fetch(url, {
method: 'GET',
headers: this.getAuthHeader(merchant),
});
@ -57,6 +69,12 @@ class ApiService {
}
const data: IApiCurrenciesResponse = await response.json();
// Only cache if no method filter was applied
if (!method) {
this.currencies = data.data;
}
return data.data;
}
@ -95,7 +113,9 @@ class ApiService {
if (!response.ok) {
const data = await response.json();
throw new Error(`Failed to initiate payment: ${response.statusText} - ${data?.error?.detail}`);
throw new Error(
`Failed to initiate payment: ${response.statusText} - ${data?.error?.detail}`
);
}
return response.json();