Added more to integration and added custom spinner

This commit is contained in:
Mitchell Magro 2026-01-02 16:00:34 +01:00
parent 2de17b8675
commit 4554bb5cce
11 changed files with 114 additions and 85 deletions

View File

@ -1,16 +1,19 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Cashier from '@/features/cashier/Cashier';
import { CashierProvider } from '@/features/cashier/context/CashierContext';
import CashierStateHandler from '@/features/cashier/CashierStateHandler/CashierStateHandler';
import PaymentStatus from '@/pages/PaymentStatus/PaymentStatus';
import '@/App.scss';
import Status from './components/Status/Status';
function App() {
return (
<div className="cashier-app">
<BrowserRouter>
<CashierProvider>
<CashierStateHandler />
<Routes>
<Route path="/" element={<Cashier />} />
<Route path="/" element={<Cashier />} errorElement={<Status type="error" message="Error loading cashier" />} />
<Route path="/result" element={<PaymentStatus />} />
</Routes>
</CashierProvider>

View File

@ -8,12 +8,28 @@
}
.loading__spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
width: 48px;
height: 48px;
border: 4px solid rgba(100, 108, 255, 0.1);
border-top: 4px solid #646cff;
border-right: 4px solid #646cff;
border-radius: 50%;
animation: spin 1s linear infinite;
animation: spin 0.8s linear infinite;
position: relative;
}
.loading__spinner::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 24px;
margin: -12px 0 0 -12px;
border: 3px solid rgba(100, 108, 255, 0.2);
border-top: 3px solid #646cff;
border-radius: 50%;
animation: spin 0.6s linear infinite reverse;
}
@keyframes spin {

View File

@ -1,10 +1,14 @@
import './Loading.scss';
function Loading() {
interface ILoadingProps {
message?: string;
}
function Loading({ message = 'Loading payment methods...' }: ILoadingProps) {
return (
<div className="loading">
<div className="loading__spinner"></div>
<p className="loading__text">Loading payment methods...</p>
<p className="loading__text">{message}</p>
</div>
);
}

View File

@ -12,13 +12,6 @@ export const TMerchant = {
export type TMerchant = (typeof TMerchant)[keyof typeof TMerchant];
export const MERCHANT_API_KEYS: Record<TMerchant, string> = {
[TMerchant.DATA_SPIN]: '10000000-0000-0000-0000-000000000001',
[TMerchant.WIN_BOT]: '20000000-0000-0000-0000-000000000002',
[TMerchant.VARK_SERVICES]: '30000000-0000-0000-0000-000000000003',
[TMerchant.BETRISE]: '40000000-0000-0000-0000-000000000004',
};
export const MERCHANT_IDS: Record<TMerchant, string> = {
[TMerchant.DATA_SPIN]: '10000000-0000-0000-0000-000000000001',
[TMerchant.WIN_BOT]: '20000000-0000-0000-0000-000000000002',
@ -27,4 +20,4 @@ export const MERCHANT_IDS: Record<TMerchant, string> = {
};
// Default merchant (can be changed based on requirements)
export const DEFAULT_MERCHANT = TMerchant.WIN_BOT;
export const DEFAULT_MERCHANT = TMerchant.BETRISE;

View File

@ -41,10 +41,13 @@ export interface ICashierConfig {
export function getCashierConfig(): ICashierConfig {
const params = new URLSearchParams(window.location.search);
const config: ICashierConfig = {};
const config: ICashierConfig = {
paymentType: (params.get('payment_type') as 'deposit' | 'withdrawal') || 'deposit',
};
// Payment type from URL (support both 'method' and 'payment_type' for backward compatibility)
const method = params.get('method');
const paymentType = params.get('payment_type') || method;
if (paymentType === 'deposit' || paymentType === 'withdrawal') {
config.paymentType = paymentType;
@ -61,10 +64,13 @@ export function getCashierConfig(): ICashierConfig {
if (amount && amount !== 'undefined') {
const parsedAmount = parseFloat(amount);
if (!isNaN(parsedAmount)) {
config.amount = parsedAmount;
config.amount = 200;
}
}
// TODO: remove this default value
config.amount = 200;
// Account from URL (for withdrawal)
const account = params.get('account');
if (account) {
@ -81,6 +87,9 @@ export function getCashierConfig(): ICashierConfig {
if (userId) {
config.userId = userId;
}
// TODO: remove this default value
config.userId = '12345';
const sessionId = params.get('sessionId');
if (sessionId) {

View File

@ -1,9 +1,6 @@
import { useEffect } from 'react';
import PaymentMethodsList from '@/features/payment-methods/PaymentMethodsList/PaymentMethodsList';
import EmptyPaymentMethods from '@/features/payment-methods/EmptyPaymentMethods/EmptyPaymentMethods';
import PaymentForm from './PaymentForm/PaymentForm';
import Loading from '@/components/Loading/Loading';
import Status from '@/components/Status/Status';
import type { IPaymentMethod } from '@/features/payment-methods/types';
import type { IPaymentRequest } from './types';
import { useCashier } from './context/CashierContext';
@ -16,26 +13,22 @@ function Cashier() {
currencies,
selectedMethod,
formMetadata,
error,
paymentUrl,
goBack,
setSelectedMethod,
initiatePayment,
} = useCashier();
// Redirect to payment URL when state is redirecting
useEffect(() => {
if (state === 'redirecting' && paymentUrl) {
window.location.href = paymentUrl;
}
}, [state, paymentUrl]);
const handleMethodSelect = (method: IPaymentMethod) => {
setSelectedMethod(method);
};
const handleFormSubmit = async (formData: Record<string, unknown>) => {
try {
const config = getCashierConfig();
if (!config.userId) {
return;
}
// Transform form data to match IPaymentRequest structure
const additionalFields: Record<string, unknown> = {};
if (formData.account) {
@ -59,13 +52,14 @@ function Cashier() {
}
});
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: '',
id: config.userId || '',
first_name: '',
last_name: '',
email: '',
@ -83,19 +77,7 @@ function Cashier() {
setSelectedMethod(null);
};
if (state === 'loading') {
return <Loading />;
}
if (state === 'error' && error) {
return <Status type="error" message={`Error: ${error.message}`} onAction={() => goBack()} />;
}
if (state === 'redirecting') {
return <div>Redirecting to payment...</div>;
}
if (selectedMethod) {
if (selectedMethod && state === 'ready' && methods.length > 0) {
return (
<PaymentForm
key={selectedMethod.code}
@ -105,7 +87,7 @@ function Cashier() {
config={getCashierConfig()}
onSubmit={handleFormSubmit}
onBack={handleBack}
isLoading={state === 'submitting'}
isLoading={false}
/>
);
}

View File

@ -0,0 +1,43 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import Loading from '@/components/Loading/Loading';
import Status from '@/components/Status/Status';
import { useCashier } from '../context/CashierContext';
function CashierStateHandler() {
const { state, error, paymentUrl, goBack } = useCashier();
const location = useLocation();
const isCashierRoute = location.pathname === '/';
// Redirect to payment URL when state is redirecting
useEffect(() => {
if (state === 'redirecting' && paymentUrl) {
window.location.href = paymentUrl;
}
}, [state, paymentUrl]);
if (!isCashierRoute) {
return null;
}
if (state === 'loading') {
return <Loading message="Loading payment methods..." />;
}
if (state === 'submitting') {
return <Loading message="Processing payment..." />;
}
if (state === 'error' && error) {
return <Status type="error" message={`Error: ${error.message}`} onAction={() => goBack()} />;
}
if (state === 'redirecting') {
return <Loading message="Redirecting to payment..." />;
}
return null;
}
export default CashierStateHandler;

View File

@ -41,7 +41,7 @@ function PaymentForm({
type: config?.paymentType || 'deposit',
method: method.code,
currency: currencies.length > 0 ? currencies[0]?.code : '',
amount: '',
amount: config?.amount || 0,
};
// Initialize customer fields from config
@ -52,7 +52,6 @@ function PaymentForm({
return initial;
});
console.log('currencies', currencies);
const parsedFields = useMemo(() => {
if (formMetadata?.length === 0) return [];
return parseFormFields(formMetadata);
@ -80,7 +79,15 @@ function PaymentForm({
return '';
}
}
return String(current || '');
// Handle numeric values including 0, and null/undefined
if (current === null || current === undefined) {
return '';
}
// For number inputs, preserve numeric values including 0
if (field.inputType === 'number' && typeof current === 'number') {
return String(current);
}
return String(current);
};
const setFieldValue = (field: IParsedField, value: string): void => {
@ -92,31 +99,6 @@ function PaymentForm({
const value = getFieldValue(field);
const fieldId = `field-${field.code}`;
if (field.inputType === 'radio' && field.code === 'type') {
return (
<div key={field.code} className={bem(block, 'radio-group')}>
<label className={bem(block, 'radio')}>
<input
type="radio"
value="deposit"
checked={value === 'deposit'}
onChange={e => setFieldValue(field, e.target.value)}
/>
<span>Deposit</span>
</label>
<label className={bem(block, 'radio')}>
<input
type="radio"
value="withdrawal"
checked={value === 'withdrawal'}
onChange={e => setFieldValue(field, e.target.value)}
/>
<span>Withdrawal</span>
</label>
</div>
);
}
if (field.inputType === 'select' && field.code === 'currency') {
return (
<div key={field.code} className={bem(block, 'field')}>
@ -202,8 +184,6 @@ function PaymentForm({
onSubmit(submitData);
};
const showPaymentType = !config.paymentType && groupedFields.payment.some(f => f.code === 'type');
return (
<div className={block}>
<div className={bem(block, 'header')}>
@ -215,13 +195,6 @@ function PaymentForm({
</div>
<form className={bem(block, 'form')} onSubmit={handleSubmit}>
{showPaymentType && groupedFields.payment.some(f => f.code === 'type') && (
<div className={bem(block, 'section')}>
<h3 className={bem(block, 'section-title')}>Payment Type</h3>
{groupedFields.payment.filter(f => f.code === 'type').map(renderField)}
</div>
)}
{groupedFields.payment.filter(f => f.code !== 'type' && f.code !== 'method').length > 0 && (
<div className={bem(block, 'section')}>
<h3 className={bem(block, 'section-title')}>Payment Information</h3>

View File

@ -59,7 +59,7 @@ class CashierService {
if (method) {
try {
const config = getCashierConfig();
const paymentType = config.paymentType || 'deposit';
const paymentType = config.paymentType || 'withdrawal';
// Fetch form metadata and currencies for the selected method in parallel
const [formMetadataData, currenciesData] = await Promise.all([

View File

@ -53,6 +53,11 @@ export function shouldShowField(
if (code === 'currency' && !config.currency) return true;
if (code === 'currency' && config.currency) return false;
// Hide customer.id field - it's set programmatically from config
if (code === 'customer.id' || (field.path[0] === 'customer' && field.path[1] === 'id')) {
return false;
}
// Show all other fields
return true;
}

View File

@ -1,4 +1,4 @@
import { API_BASE_URL, MERCHANT_API_KEYS, TMerchant, DEFAULT_MERCHANT } from '@/config/api';
import { API_BASE_URL, MERCHANT_IDS, TMerchant, DEFAULT_MERCHANT } from '@/config/api';
import type {
IApiMethodsResponse,
IApiCurrency,
@ -16,7 +16,7 @@ class ApiService {
private currencies: IApiCurrency[] = []; // Cache for currencies without method filter
private getAuthHeader(merchant: TMerchant = DEFAULT_MERCHANT): HeadersInit {
const apiKey = MERCHANT_API_KEYS[merchant];
const apiKey = MERCHANT_IDS[merchant];
return {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
@ -88,6 +88,7 @@ class ApiService {
type,
});
console.log('params', params.toString());
const response = await fetch(`${API_BASE_URL}/form?${params.toString()}`, {
method: 'GET',
headers: this.getAuthHeader(merchant),