Created Payment methods page
This commit is contained in:
parent
0149c0f341
commit
e723dde47f
@ -1,6 +1,6 @@
|
||||
// App component styles using BEM methodology
|
||||
|
||||
.app {
|
||||
.cashier-app {
|
||||
// Root container
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
@ -8,7 +8,7 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app__logo {
|
||||
.cashier-app__logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
@ -40,11 +40,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.app__card {
|
||||
.cashier-app__card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.app__read-the-docs {
|
||||
.cashier-app__read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
|
||||
24
src/App.tsx
24
src/App.tsx
@ -1,25 +1,11 @@
|
||||
import PaymentMethodsList from '@/features/payment-methods/PaymentMethodsList/PaymentMethodsList'
|
||||
import EmptyPaymentMethods from '@/features/payment-methods/EmptyPaymentMethods/EmptyPaymentMethods'
|
||||
import type { IPaymentMethod } from '@/features/payment-methods/types'
|
||||
import { PAYMENT_METHODS } from '@/constants/payment'
|
||||
import './App.scss'
|
||||
import Cashier from '@/features/cashier/Cashier'
|
||||
import '@/App.scss'
|
||||
|
||||
function App() {
|
||||
const handleMethodSelect = (method: IPaymentMethod) => {
|
||||
console.log('Selected payment method:', method)
|
||||
// Handle payment method selection here
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{PAYMENT_METHODS.length === 0 ? (
|
||||
<EmptyPaymentMethods />
|
||||
) : (
|
||||
<PaymentMethodsList
|
||||
methods={PAYMENT_METHODS}
|
||||
onMethodSelect={handleMethodSelect}
|
||||
/>
|
||||
)}
|
||||
<div className="cashier-app">
|
||||
<Cashier />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
30
src/components/Error/Error.scss
Normal file
30
src/components/Error/Error.scss
Normal file
@ -0,0 +1,30 @@
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.error__message {
|
||||
color: #d32f2f;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error__retry {
|
||||
padding: 0.5rem 1.5rem;
|
||||
background-color: #646cff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #535bf2;
|
||||
}
|
||||
}
|
||||
|
||||
22
src/components/Error/Error.tsx
Normal file
22
src/components/Error/Error.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import './Error.scss'
|
||||
|
||||
interface IErrorProps {
|
||||
message: string;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
function Error({ message, onRetry }: IErrorProps) {
|
||||
return (
|
||||
<div className="error">
|
||||
<p className="error__message">{message}</p>
|
||||
{onRetry && (
|
||||
<button className="error__retry" onClick={onRetry}>
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Error;
|
||||
|
||||
28
src/components/Loading/Loading.scss
Normal file
28
src/components/Loading/Loading.scss
Normal file
@ -0,0 +1,28 @@
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.loading__spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #646cff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading__text {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
13
src/components/Loading/Loading.tsx
Normal file
13
src/components/Loading/Loading.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import './Loading.scss'
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<div className="loading">
|
||||
<div className="loading__spinner"></div>
|
||||
<p className="loading__text">Loading payment methods...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
|
||||
31
src/config/api.ts
Normal file
31
src/config/api.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// Use proxy in development, direct URL in production
|
||||
export const API_BASE_URL = import.meta.env.DEV
|
||||
? '/api/v1'
|
||||
: 'https://cashier-backend.brgoperations.com/api/v1';
|
||||
|
||||
export const TMerchant = {
|
||||
DATA_SPIN: 'DATA_SPIN',
|
||||
WIN_BOT: 'WIN_BOT',
|
||||
VARK_SERVICES: 'VARK_SERVICES',
|
||||
BETRISE: 'BETRISE',
|
||||
} as const;
|
||||
|
||||
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',
|
||||
[TMerchant.VARK_SERVICES]: '30000000-0000-0000-0000-000000000003',
|
||||
[TMerchant.BETRISE]: '40000000-0000-0000-0000-000000000004',
|
||||
};
|
||||
|
||||
// Default merchant (can be changed based on requirements)
|
||||
export const DEFAULT_MERCHANT = TMerchant.WIN_BOT;
|
||||
|
||||
0
src/features/cashier/Cashier.scss
Normal file
0
src/features/cashier/Cashier.scss
Normal file
38
src/features/cashier/Cashier.tsx
Normal file
38
src/features/cashier/Cashier.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import PaymentMethodsList from '@/features/payment-methods/PaymentMethodsList/PaymentMethodsList'
|
||||
import EmptyPaymentMethods from '@/features/payment-methods/EmptyPaymentMethods/EmptyPaymentMethods'
|
||||
import Loading from '@/components/Loading/Loading'
|
||||
import Error from '@/components/Error/Error'
|
||||
import type { IPaymentMethod } from '@/features/payment-methods/types'
|
||||
import { usePaymentMethods } from './hooks/usePaymentMethods'
|
||||
|
||||
function Cashier() {
|
||||
const { methods: apiMethods, loading, error } = usePaymentMethods();
|
||||
|
||||
const handleMethodSelect = (method: IPaymentMethod) => {
|
||||
console.log('Selected payment method:', method)
|
||||
// Handle payment method selection here
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Error message={`Error loading payment methods: ${error.message}`} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{apiMethods.length === 0 ? (
|
||||
<EmptyPaymentMethods />
|
||||
) : (
|
||||
<PaymentMethodsList
|
||||
methods={apiMethods}
|
||||
onMethodSelect={handleMethodSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cashier
|
||||
30
src/features/cashier/hooks/useCurrencies.ts
Normal file
30
src/features/cashier/hooks/useCurrencies.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { apiService } from '@/services/api';
|
||||
import type { IApiCurrency } from '../types';
|
||||
import type { TMerchant } from '@/config/api';
|
||||
|
||||
export function useCurrencies(merchant?: TMerchant) {
|
||||
const [currencies, setCurrencies] = useState<IApiCurrency[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrencies = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await apiService.getCurrencies(merchant);
|
||||
setCurrencies(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to fetch currencies'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCurrencies();
|
||||
}, [merchant]);
|
||||
|
||||
return { currencies, loading, error };
|
||||
}
|
||||
|
||||
44
src/features/cashier/hooks/usePayment.ts
Normal file
44
src/features/cashier/hooks/usePayment.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { apiService } from '@/services/api';
|
||||
import { MERCHANT_IDS, DEFAULT_MERCHANT, type TMerchant } from '@/config/api';
|
||||
import type { IPaymentRequest, IPaymentResponse } from '../types';
|
||||
|
||||
export function usePayment() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [paymentUrl, setPaymentUrl] = useState<string | null>(null);
|
||||
|
||||
const initiatePayment = async (
|
||||
paymentRequest: Omit<IPaymentRequest, 'merchant_id'>,
|
||||
merchant: TMerchant = DEFAULT_MERCHANT
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setPaymentUrl(null);
|
||||
|
||||
const fullRequest: IPaymentRequest = {
|
||||
...paymentRequest,
|
||||
merchant_id: MERCHANT_IDS[merchant],
|
||||
};
|
||||
|
||||
const response = await apiService.initiatePayment(fullRequest, merchant);
|
||||
setPaymentUrl(response.payment_url);
|
||||
return response;
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to initiate payment');
|
||||
setError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
initiatePayment,
|
||||
loading,
|
||||
error,
|
||||
paymentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
30
src/features/cashier/hooks/usePaymentMethods.ts
Normal file
30
src/features/cashier/hooks/usePaymentMethods.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { apiService } from '@/services/api';
|
||||
import type { TMerchant } from '@/config/api';
|
||||
import type { IPaymentMethod } from '@/features/payment-methods/types';
|
||||
|
||||
export function usePaymentMethods(merchant?: TMerchant) {
|
||||
const [methods, setMethods] = useState<IPaymentMethod[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMethods = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await apiService.getMethods(merchant);
|
||||
setMethods(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to fetch payment methods'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMethods();
|
||||
}, [merchant]);
|
||||
|
||||
return { methods, loading, error };
|
||||
}
|
||||
|
||||
50
src/features/cashier/types.ts
Normal file
50
src/features/cashier/types.ts
Normal file
@ -0,0 +1,50 @@
|
||||
export interface IApiMethod {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IApiMethodsResponse {
|
||||
data: IApiMethod[];
|
||||
}
|
||||
|
||||
export interface IApiCurrency {
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface IApiCurrenciesResponse {
|
||||
data: IApiCurrency[];
|
||||
}
|
||||
|
||||
export interface ICustomer {
|
||||
id?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
account?: string; // IBAN or similar - required on withdrawal
|
||||
ip?: string;
|
||||
country?: string;
|
||||
city?: string;
|
||||
zip?: string;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export interface IRedirect {
|
||||
success: string; // URL where to redirect after payment success
|
||||
cancel: string; // URL where to redirect after payment cancel
|
||||
error: string; // URL where to redirect if error occur
|
||||
}
|
||||
|
||||
export interface IPaymentRequest {
|
||||
merchant_id: string; // same id as in header
|
||||
payment_type: 'deposit' | 'withdrawal';
|
||||
method: string;
|
||||
customer: ICustomer;
|
||||
currency: string;
|
||||
amount: number; // float64
|
||||
redirect: IRedirect;
|
||||
}
|
||||
|
||||
export interface IPaymentResponse {
|
||||
payment_url: string; // URL where customer needs to continue with payment flow
|
||||
}
|
||||
|
||||
26
src/features/cashier/utils/methodMapper.ts
Normal file
26
src/features/cashier/utils/methodMapper.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { IApiMethod } from '../types';
|
||||
import type { IPaymentMethod } from '@/features/payment-methods/types';
|
||||
|
||||
const METHOD_TYPES: Record<string, string> = {
|
||||
bankin: 'Banking',
|
||||
bankpay: 'Banking',
|
||||
papel: 'Digital Wallet',
|
||||
mefete: 'Digital Wallet',
|
||||
pep: 'Digital Wallet',
|
||||
};
|
||||
|
||||
export function mapApiMethodToPaymentMethod(apiMethod: IApiMethod, index: number): IPaymentMethod {
|
||||
const methodCode = apiMethod.code.toLowerCase();
|
||||
|
||||
return {
|
||||
id: `${methodCode}-${index}`,
|
||||
name: apiMethod.name, // Use the name from API
|
||||
type: METHOD_TYPES[methodCode] || 'Payment',
|
||||
isActive: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapApiMethodsToPaymentMethods(apiMethods: IApiMethod[]): IPaymentMethod[] {
|
||||
return apiMethods.map((apiMethod, index) => mapApiMethodToPaymentMethod(apiMethod, index));
|
||||
}
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from '@/App.tsx'
|
||||
import './index.scss'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
<App />
|
||||
)
|
||||
|
||||
75
src/services/api.ts
Normal file
75
src/services/api.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { API_BASE_URL, MERCHANT_API_KEYS, TMerchant, DEFAULT_MERCHANT } from '@/config/api';
|
||||
import type { IApiMethodsResponse, IApiCurrency, IApiCurrenciesResponse, IPaymentRequest, IPaymentResponse } from '@/features/cashier/types';
|
||||
import { mapApiMethodsToPaymentMethods } from '@/features/cashier/utils/methodMapper';
|
||||
import type { IPaymentMethod } from '@/features/payment-methods/types';
|
||||
|
||||
class ApiService {
|
||||
|
||||
private methods: IPaymentMethod[] = []
|
||||
private currencies: IApiCurrency[] = []
|
||||
|
||||
private getAuthHeader(merchant: TMerchant = DEFAULT_MERCHANT): HeadersInit {
|
||||
const apiKey = MERCHANT_API_KEYS[merchant];
|
||||
return {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
async getMethods(merchant: TMerchant = DEFAULT_MERCHANT): Promise<IPaymentMethod[]> {
|
||||
if (this.methods.length > 0) {
|
||||
return this.methods
|
||||
}
|
||||
const response = await fetch(`${API_BASE_URL}/methods`, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeader(merchant),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch methods: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: IApiMethodsResponse = await response.json();
|
||||
|
||||
this.methods = mapApiMethodsToPaymentMethods(data.data);
|
||||
|
||||
return this.methods;
|
||||
}
|
||||
|
||||
async getCurrencies(merchant: TMerchant = DEFAULT_MERCHANT): Promise<IApiCurrency[]> {
|
||||
if (this.currencies.length > 0) {
|
||||
return this.currencies
|
||||
}
|
||||
const response = await fetch(`${API_BASE_URL}/currencies`, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeader(merchant),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch currencies: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: IApiCurrenciesResponse = await response.json();
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async initiatePayment(
|
||||
paymentRequest: IPaymentRequest,
|
||||
merchant: TMerchant = DEFAULT_MERCHANT
|
||||
): Promise<IPaymentResponse> {
|
||||
const response = await fetch(`${API_BASE_URL}/payment`, {
|
||||
method: 'POST',
|
||||
headers: this.getAuthHeader(merchant),
|
||||
body: JSON.stringify(paymentRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to initiate payment: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = new ApiService();
|
||||
|
||||
@ -13,4 +13,13 @@ export default defineConfig({
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://cashier-backend.brgoperations.com',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user