Kiến trúc & Tech stack
Tech stack
| Lớp | Công nghệ |
|---|---|
| Framework | React 18 + TypeScript 4.9 |
| Build | Create React App 5 + CRACO 7 |
| Routing | react-router-dom v6 (BrowserRouter) |
| Styling | Tailwind CSS 3 + CSS variables (design tokens) |
| HTTP | Axios 1.3 |
| Forms | react-hook-form 7 |
| Toast | react-hot-toast |
| Icons | lucide-react |
| CAPTCHA | @marsidev/react-turnstile (Cloudflare Turnstile) |
| Phone input | react-phone-number-input |
| QR / Poster | qrcode, html-to-image |
| Date | date-fns |
| Mobile | Capacitor 7 (Android wrapper, remote URL mode) |
| Monorepo | @kioskgaming/ui, @kioskgaming/page-loading |
Cấu trúc thư mục
kioskgaming_shop/
├── src/
│ ├── index.tsx # Entry point, đăng ký Service Worker
│ ├── App.tsx # Router + AuthProvider
│ ├── index.css # Tailwind + CSS variables
│ ├── config/env.ts # Biến môi trường tập trung
│ ├── constants/index.ts # API URL, storage keys, custom events
│ ├── types/index.ts # TypeScript interfaces
│ ├── services/api.ts # HTTP client + toàn bộ API functions
│ ├── hooks/ # useAuth, ledger hooks, zerox methods
│ ├── pages/ # 18 route pages
│ ├── components/
│ │ ├── auth/ # Login, forgot password, ProtectedRoute
│ │ ├── layout/ # AppLayout (sidebar, header, nav)
│ │ ├── credits/ # Mua credits, crypto wallets
│ │ ├── dashboard/ # Dashboard overview
│ │ ├── finance/ # Withdrawal limits panel
│ │ ├── players/ # Player detail panel
│ │ ├── poster/ # Promo poster templates
│ │ └── ui/ # ModalPortal, empty states
│ └── utils/ # format, ban, phone, ledger helpers
├── public/ # Static assets, PWA manifest, service worker
├── android/ # Capacitor Android wrapper
├── scripts/ # set-android-app-name.js
├── craco.config.js # CRA override (monorepo UI package)
├── capacitor.config.ts # Capacitor remote URL config
├── tailwind.config.js
├── vercel.json # SPA rewrite
└── deploy/nginx-spa.conf.example
Entry points
| File | Vai trò |
|---|---|
src/index.tsx | Mount React, register Service Worker (production only) |
src/App.tsx | Định nghĩa routes, bọc AuthProvider |
src/services/api.ts | Layer tích hợp backend (~1146 dòng) |
State management
Không dùng Redux/Zustand. Kiến trúc state:
| Pattern | Vị trí | Mục đích |
|---|---|---|
| React Context | hooks/useAuth.tsx | Auth state (user, token, login/logout) |
| Local useState | Mỗi page/component | UI state, form, modal |
| Custom hooks | useShopWalletTransactions, useShopOnlineBalanceLedger, useSupportedZeroxMethods | Data fetching có reload |
| localStorage | Auth tokens, user data, zerox methods cache (24h TTL) | |
| Custom Events | window.dispatchEvent | Cross-component sync |
Custom events
| Event | Constant | Khi nào dispatch |
|---|---|---|
shop-auth-token-changed | SHOP_AUTH_TOKEN_CHANGED_EVENT | Login / logout |
shop-credit-balance-refresh | SHOP_CREDIT_BALANCE_REFRESH_EVENT | Sau flow thay đổi số dư |
auth:unauthorized | — | Axios interceptor nhận 401 |
Storage keys
| Key | Nội dung |
|---|---|
shop_auth_token | Bearer token shop |
shop_user_data | Profile shop (JSON) |
cashier_auth_token | Token cashier (client riêng, không phải flow chính) |
cashier_user_data | Profile cashier |
Authentication flow
sequenceDiagram
participant U as Shop user
participant S as Shop Portal
participant B as Backend
U->>S: Nhập loginId + password
S->>B: POST /portal-auth/login
alt Cần OTP (2FA)
B-->>S: needsOtp + pendingToken
U->>S: Nhập OTP
S->>B: POST /shop-auth/verify-otp
end
B-->>S: accessToken + shop profile
S->>S: Lưu token vào localStorage
S->>B: GET /shop/me (refresh profile)
- Header:
Authorization: Bearer {token} - 401 → dispatch
auth:unauthorized→ auto logout ProtectedRouteredirect về/loginnếu chưa auth
Biến môi trường
| Biến | Bắt buộc | Mô tả | Default |
|---|---|---|---|
REACT_APP_API_BASE_URL | Có | Backend API base URL | http://localhost:3001/api |
REACT_APP_TURNSTILE_ENABLED | Không | Bật Cloudflare Turnstile | false |
REACT_APP_TURNSTILE_SITE_KEY | Khi bật Turnstile | Site key | '' |
REACT_APP_APP_NAME | Không | Tên app hiển thị | Kiosk Gaming — Shop |
REACT_APP_CLIENT_TELEGRAM_SUPPORT_URL | Không | Link Telegram support | — |
REACT_APP_CLIENT_ANDROID_APK_URL | Không | Link tải APK Android | — |
REACT_APP_CLIENT_IOS_VIDEO_URL | Không | Video hướng dẫn iOS | — |
PORT | Không | Dev server port | 3004 |
CAP_SERVER_URL | Build APK | Remote URL cho Capacitor | https://shop.kioskservice.club/ |
warning
Backend cần set SHOP_FRONTEND_URL trỏ về origin của app để redirect payment về /payment/success và /payment/cancelled.
Scripts npm
| Script | Mô tả |
|---|---|
npm start | Dev server port 3004 (CRACO) |
npm run build | Production build |
npm run cap:sync:dev | Sync Capacitor → https://dev-shop.kioskservice.club/ |
npm run cap:sync:prod | Sync Capacitor → https://shop.kioskservice.club/ |
npm run apk:dev / apk:prod | Build APK debug |
Cấu hình đặc biệt
CRACO + Monorepo UI
craco.config.js import configureKioskgamingUi từ ../packages/ui để webpack resolve shared UI package.
Capacitor Android
- App ID:
club.kioskservice.kioskgaming.shop - Remote URL mode: không bundle web assets, load từ URL remote
- Dev:
https://dev-shop.kioskservice.club/ - Prod:
https://shop.kioskservice.club/
SPA routing (production)
| Platform | Config |
|---|---|
| Vercel | vercel.json rewrite /(.*) → /index.html |
| Netlify/Cloudflare | public/_redirects |
| Nginx | deploy/nginx-spa.conf.example (try_files) |
Service Worker
public/sw.js+serviceWorkerRegistration.ts- Chỉ register khi
NODE_ENV === 'production'
Shared UI package
Components dùng chung từ @kioskgaming/ui:
PortalCreditTransactionsLedger,PortalUsdWalletLedgerTableCreditTransactionDetailModal,UsdWalletTransactionDetailModalWithdrawOtpStep,WithdrawBlockedAlertFormField,Modal,Button,Input,Select,TextareaPlayerFraudIndicatorsPanel,SupportDownloadRail
Sơ đồ tổng quan
flowchart TB
subgraph Client["kioskgaming_shop"]
Index[index.tsx] --> App[App.tsx]
App --> Auth[AuthProvider]
Auth --> Router[React Router]
Router --> Public["/login, /forgot-password"]
Router --> Protected["ProtectedRoute + Pages"]
Protected --> Layout[AppLayout]
Layout --> API[services/api.ts]
end
subgraph Backend["kioskgaming_backend"]
API -->|Bearer token| ShopAPI["/shop/*"]
API --> AuthAPI["/shop-auth/*, /portal-auth/*"]
API --> PaymentAPI["/payment/*"]
end
subgraph Storage["Browser"]
Auth --> LS["localStorage: shop_auth_token"]
end