diff --git a/src/components/Icons/ApprovedIcon.tsx b/src/components/Icons/ApprovedIcon.tsx
new file mode 100644
index 0000000..c9f7fef
--- /dev/null
+++ b/src/components/Icons/ApprovedIcon.tsx
@@ -0,0 +1,24 @@
+interface IApprovedIconProps {
+ size?: number;
+}
+
+function ApprovedIcon({ size = 20 }: IApprovedIconProps) {
+ return (
+
+ );
+}
+
+export default ApprovedIcon;
diff --git a/src/components/Icons/CancelledIcon.tsx b/src/components/Icons/CancelledIcon.tsx
new file mode 100644
index 0000000..998b44b
--- /dev/null
+++ b/src/components/Icons/CancelledIcon.tsx
@@ -0,0 +1,25 @@
+interface ICancelledIconProps {
+ size?: number;
+}
+
+function CancelledIcon({ size = 20 }: ICancelledIconProps) {
+ return (
+
+ );
+}
+
+export default CancelledIcon;
diff --git a/src/components/Icons/ErrorIcon.tsx b/src/components/Icons/ErrorIcon.tsx
new file mode 100644
index 0000000..a6f10d6
--- /dev/null
+++ b/src/components/Icons/ErrorIcon.tsx
@@ -0,0 +1,25 @@
+interface IErrorIconProps {
+ size?: number;
+}
+
+function ErrorIcon({ size = 20 }: IErrorIconProps) {
+ return (
+
+ );
+}
+
+export default ErrorIcon;
diff --git a/src/components/Status/Status.scss b/src/components/Status/Status.scss
index 8a27381..0bacc89 100644
--- a/src/components/Status/Status.scss
+++ b/src/components/Status/Status.scss
@@ -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;
+}
diff --git a/src/components/Status/Status.tsx b/src/components/Status/Status.tsx
index 77aaf9b..ea13e89 100644
--- a/src/components/Status/Status.tsx
+++ b/src/components/Status/Status.tsx
@@ -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 (
+ {type === 'cancel' && (
+
+
+
+ )}
+ {type === 'success' && (
+
+ )}
+ {type === 'error' && (
+
+
+
+ )}
{message}
{onAction &&
}
diff --git a/src/features/cashier/Cashier.tsx b/src/features/cashier/Cashier.tsx
index cc1f413..687aa9c 100644
--- a/src/features/cashier/Cashier.tsx
+++ b/src/features/cashier/Cashier.tsx
@@ -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 {
@@ -42,11 +41,19 @@ function Cashier() {
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)
+ ![
+ 'payment_type',
+ 'method',
+ 'currency',
+ 'amount',
+ 'customer',
+ 'redirect',
+ 'account',
+ ].includes(key)
) {
additionalFields[key] = value;
}
@@ -81,7 +88,7 @@ function Cashier() {
}
if (state === 'error' && error) {
- return goBack()}/>;
+ return goBack()} />;
}
if (state === 'redirecting') {
@@ -91,6 +98,7 @@ function Cashier() {
if (selectedMethod) {
return (
= {
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]);
diff --git a/src/features/cashier/context/CashierContext.tsx b/src/features/cashier/context/CashierContext.tsx
index 66ad641..8f8abae 100644
--- a/src/features/cashier/context/CashierContext.tsx
+++ b/src/features/cashier/context/CashierContext.tsx
@@ -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;
}, []);
diff --git a/src/features/cashier/services/CashierService.ts b/src/features/cashier/services/CashierService.ts
index 2a3f546..c76d1a2 100644
--- a/src/features/cashier/services/CashierService.ts
+++ b/src/features/cashier/services/CashierService.ts
@@ -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) {
diff --git a/src/features/cashier/utils/formBuilder.ts b/src/features/cashier/utils/formBuilder.ts
index 43b3c07..bc09637 100644
--- a/src/features/cashier/utils/formBuilder.ts
+++ b/src/features/cashier/utils/formBuilder.ts
@@ -12,7 +12,7 @@ export function parseFieldCode(code: string): string[] {
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';
@@ -21,7 +21,7 @@ export function getInputType(code: string): string {
if (lowerCode === 'type') return 'radio';
if (lowerCode === 'method') return 'select';
if (lowerCode === 'currency') return 'select';
-
+
return 'text';
}
@@ -41,18 +41,18 @@ export function shouldShowField(
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;
}
@@ -60,12 +60,12 @@ export function shouldShowField(
export function buildNestedObject(path: string[], value: unknown): Record {
const result: Record = {};
let current = result;
-
+
for (let i = 0; i < path.length - 1; i++) {
current[path[i]] = {};
current = current[path[i]] as Record;
}
-
+
current[path[path.length - 1]] = value;
return result;
}
@@ -75,7 +75,7 @@ export function mergeNestedObjects(
obj2: Record
): Record {
const result = { ...obj1 };
-
+
for (const key in obj2) {
if (typeof obj2[key] === 'object' && obj2[key] !== null && !Array.isArray(obj2[key])) {
result[key] = mergeNestedObjects(
@@ -86,7 +86,7 @@ export function mergeNestedObjects(
result[key] = obj2[key];
}
}
-
+
return result;
}
@@ -98,7 +98,7 @@ export function groupFieldsBySection(fields: 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);
@@ -108,7 +108,6 @@ export function groupFieldsBySection(fields: IParsedField[]): {
payment.push(field);
}
});
-
+
return { payment, customer, account };
}
-
diff --git a/src/pages/PaymentStatus/PaymentStatus.tsx b/src/pages/PaymentStatus/PaymentStatus.tsx
index 768a15f..abdf3b7 100644
--- a/src/pages/PaymentStatus/PaymentStatus.tsx
+++ b/src/pages/PaymentStatus/PaymentStatus.tsx
@@ -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 (
{
- if (this.currencies.length > 0) {
+ async getCurrencies(
+ merchant: TMerchant = DEFAULT_MERCHANT,
+ method?: string
+ ): Promise {
+ // 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();