diff --git a/src/App.scss b/src/App.scss
index 230f20f..4d7abed 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -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;
}
diff --git a/src/App.tsx b/src/App.tsx
index 489c642..f18b9dc 100644
--- a/src/App.tsx
+++ b/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 (
-
- {PAYMENT_METHODS.length === 0 ? (
-
- ) : (
-
- )}
+
+
)
}
diff --git a/src/components/Error/Error.scss b/src/components/Error/Error.scss
new file mode 100644
index 0000000..ddc4c28
--- /dev/null
+++ b/src/components/Error/Error.scss
@@ -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;
+ }
+}
+
diff --git a/src/components/Error/Error.tsx b/src/components/Error/Error.tsx
new file mode 100644
index 0000000..6a02a8c
--- /dev/null
+++ b/src/components/Error/Error.tsx
@@ -0,0 +1,22 @@
+import './Error.scss'
+
+interface IErrorProps {
+ message: string;
+ onRetry?: () => void;
+}
+
+function Error({ message, onRetry }: IErrorProps) {
+ return (
+
+
{message}
+ {onRetry && (
+
+ )}
+
+ );
+}
+
+export default Error;
+
diff --git a/src/components/Loading/Loading.scss b/src/components/Loading/Loading.scss
new file mode 100644
index 0000000..d727a89
--- /dev/null
+++ b/src/components/Loading/Loading.scss
@@ -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;
+}
+
diff --git a/src/components/Loading/Loading.tsx b/src/components/Loading/Loading.tsx
new file mode 100644
index 0000000..f4a0a9a
--- /dev/null
+++ b/src/components/Loading/Loading.tsx
@@ -0,0 +1,13 @@
+import './Loading.scss'
+
+function Loading() {
+ return (
+
+
+
Loading payment methods...
+
+ );
+}
+
+export default Loading;
+
diff --git a/src/config/api.ts b/src/config/api.ts
new file mode 100644
index 0000000..cc3101e
--- /dev/null
+++ b/src/config/api.ts
@@ -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.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.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;
+
diff --git a/src/features/cashier/Cashier.scss b/src/features/cashier/Cashier.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/features/cashier/Cashier.tsx b/src/features/cashier/Cashier.tsx
new file mode 100644
index 0000000..ffb6840
--- /dev/null
+++ b/src/features/cashier/Cashier.tsx
@@ -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
+ }
+
+ if (error) {
+ return
+ }
+
+ return (
+ <>
+ {apiMethods.length === 0 ? (
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+export default Cashier
diff --git a/src/features/cashier/hooks/useCurrencies.ts b/src/features/cashier/hooks/useCurrencies.ts
new file mode 100644
index 0000000..e018eaf
--- /dev/null
+++ b/src/features/cashier/hooks/useCurrencies.ts
@@ -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([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(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 };
+}
+
diff --git a/src/features/cashier/hooks/usePayment.ts b/src/features/cashier/hooks/usePayment.ts
new file mode 100644
index 0000000..81bf40d
--- /dev/null
+++ b/src/features/cashier/hooks/usePayment.ts
@@ -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(null);
+ const [paymentUrl, setPaymentUrl] = useState(null);
+
+ const initiatePayment = async (
+ paymentRequest: Omit,
+ 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,
+ };
+}
+
diff --git a/src/features/cashier/hooks/usePaymentMethods.ts b/src/features/cashier/hooks/usePaymentMethods.ts
new file mode 100644
index 0000000..a004576
--- /dev/null
+++ b/src/features/cashier/hooks/usePaymentMethods.ts
@@ -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([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(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 };
+}
+
diff --git a/src/features/cashier/types.ts b/src/features/cashier/types.ts
new file mode 100644
index 0000000..4c1f4a7
--- /dev/null
+++ b/src/features/cashier/types.ts
@@ -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
+}
+
diff --git a/src/features/cashier/utils/methodMapper.ts b/src/features/cashier/utils/methodMapper.ts
new file mode 100644
index 0000000..24e851d
--- /dev/null
+++ b/src/features/cashier/utils/methodMapper.ts
@@ -0,0 +1,26 @@
+import type { IApiMethod } from '../types';
+import type { IPaymentMethod } from '@/features/payment-methods/types';
+
+const METHOD_TYPES: Record = {
+ 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));
+}
+
diff --git a/src/main.tsx b/src/main.tsx
index 28bbff5..7d831d3 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -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(
-
-
- ,
+
)
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 0000000..ea14f85
--- /dev/null
+++ b/src/services/api.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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();
+
diff --git a/vite.config.ts b/vite.config.ts
index 574f6c2..0f3cc94 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -13,4 +13,13 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
+ server: {
+ proxy: {
+ '/api': {
+ target: 'https://cashier-backend.brgoperations.com',
+ changeOrigin: true,
+ secure: true,
+ },
+ },
+ },
})