Added more to integration and added custom spinner
This commit is contained in:
parent
2de17b8675
commit
4554bb5cce
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user