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();