Shop Portal
(Cổng Quản Lý Cửa Hàng)
Giao diện B2B cho chủ cửa hàng gaming kiosk. Quản lý player, cashier/shift, nạp/rút credits, ví USD online, kiosk domain, counter deposits/redemptions — trong chuỗi SuperAgent → Agent → Shop → Cashier → Player.
Shop nằm ở tầng giữa của hệ thống phân cấp. Họ mua credits từ Agent cấp trên và phân phối xuống Player tại địa điểm kinh doanh thực tế.
Mỗi shop có một ví credit riêng. Credit mua từ Agent, sau đó phân phối cho Player khi họ chơi game. Số dư hiển thị trên Dashboard với cảnh báo low-balance.
Mỗi shop nhận một cost_rate do Agent thiết lập. Ví dụ: rate 0.85 nghĩa là mua 100 credits sẽ tốn $85 USD. Rate hiển thị trên Dashboard card.
Mỗi shop được cấu hình một subdomain riêng tại tenShop.kioskgaming.com — Player truy cập URL này để chơi game tại địa điểm cửa hàng.
DashboardPage.tsx — Trang chủ hiển thị 4 stat card chính và danh sách giao dịch gần đây.
| Stat Card | Icon | Giá trị | Loại |
|---|---|---|---|
| Shop wallet | Coins |
Credit balance (2 chữ số thập phân) | purchase — mở modal mua credits |
| Players | Users |
Số player đã liên kết | link → /players |
| Cost rate | Percent |
Tỷ lệ % từ Agent (ví dụ: 85.00%) | plain — chỉ xem |
| Kiosk URL | Globe |
"Setup" hoặc subdomain hiện tại | link → /kiosk-domain |
Dashboard sử dụng location.state.openCreditPurchase để mở modal mua credits khi điều hướng về từ trang ShopCreditPurchaseModal. Sau khi đặt hàng xong, dashboard tự refresh balance trong nền (silent mode, không hiện loading spinner).
ShopCreditPurchaseTrigger— nút mở credit purchase với tone màu orangeShopDashboardOverview— thông tin shop (name, email, status)ShopHomeRecentTransactions— lịch sử giao dịch gần đây (appearance="dashboard")
PlayersPage.tsx — Danh sách player, tìm kiếm, sắp xếp, phân trang. Hai modal chính: Create Player và Transfer from Another Shop.
- Responsive: card view trên mobile, table trên desktop (≥md)
- Search: debounce 300ms, tìm theo phone hoặc email
- Sort:
shop_assigned_at(mặc định, desc) hoặcname - Phân trang: 10/20/50 rows, hiển thị tổng số player
- Click username →
/players/:playerId - Status badge:
active(emerald) hoặc khác (gray)
- Create player — mở modal tạo mới hoặc liên kết player chưa có shop (flow="create")
- Transfer from another shop — mở modal chuyển player đang ở shop khác sang (flow="transfer")
- Mỗi modal dùng
ShopPlayerLinkByPhonePanelvới propflowkhác nhau - Modal key được reset khi mở lại để clear state cũ
- Hai flow có thể switch lẫn nhau qua callback
onSwitchFlow
ShopPlayerLinkByPhonePanel — Lõi logic liên kết player
Component dùng chung cho cả hai flow. Tích hợp react-phone-number-input để nhập số điện thoại quốc tế với country picker.
OTP Registration Flow (Create Player)
Hỗ trợ resend OTP với cooldown timer (nextResendAt) và resendCount tracking. Verification method: SMS hoặc email.
PlayerDetailPage — Chi tiết Player
/players/:playerId — PlayerDetailPage.tsx — Xem và quản lý từng player. Tích hợp PlayerDetailPanel component cho UI chi tiết.
- Load player data và shop credit balance song song
- Dispatch
shopCreditBalanceRefreshcustom event sau mỗi thao tác credit - 404 empty state nếu playerId không hợp lệ hoặc không thuộc shop
- Breadcrumb back →
/players
/transfer redirect đến /players (legacy). Trang TransferToPlayerPage.tsx hiện ở /transfer/legacy — chức năng này được tích hợp trực tiếp vào modal của PlayersPage thông qua ShopPlayerLinkByPhonePanel.
Trang /transfer/legacy là màn hình tổng hợp: lookup player bằng phone, liên kết/tạo player (OTP), và nạp credits cho player đã có trong shop.
- Lookup phone → status =
other_shop initiatePlayerTransfer(playerId)→ nhận transfer ID- OTP gửi đến phone của player (họ phải xác nhận)
verifyPlayerTransfer(transferId, otp)→ player chuyển sang shop này- Sau verify, lookup lại để cập nhật trạng thái UI
- Chỉ hoạt động khi player status =
in_shop - Nhập amount (number input, step=any) và payment method
- Payment method:
cashhoặcbank transfer - Confirmation modal với bảng chi tiết (balance before/after)
- Kiểm tra insufficient funds ngay trên form
postShopPlayerCredits(playerId, {action:'add', amount, payment_method})
Hai loại OTP trong TransferToPlayerPage
| otpKind | Khi nào | API khởi tạo | API verify |
|---|---|---|---|
create |
Player chưa tồn tại (not_found) hoặc chưa có shop (unassigned) |
registerPlayerInitiate(phone) |
verifyPlayerRegistration(id, otp) |
assign |
Player đang ở shop khác (other_shop) |
initiatePlayerTransfer(playerId) |
verifyPlayerTransfer(id, otp) |
Khi điều hướng đến /transfer/legacy với location.state.prefillPhone, trang tự động điền số điện thoại và chạy lookup ngay. Sau đó clear state để tránh prefill lại khi refresh.
KioskDomainPage.tsx — /kiosk-domain — Mỗi cửa hàng có một URL kiosk riêng dạng tenShop.kioskgaming.com. Trang này cho phép tạo mới hoặc thay đổi subdomain.
Ba trạng thái UI
Form với input "Subdomain label", preview URL live, nút "Create kiosk URL". Validation: chỉ chấp nhận lowercase, số, gạch ngang.
Hiển thị URL hiện tại dưới dạng https://preview. Nút "Edit" (Pencil icon) chuyển sang form sửa.
Form thay đổi subdomain với warning "Changing the URL may break old bookmarks until DNS updates propagate." Có nút Cancel và Save.
API & Logic
| Hàm | Mô tả |
|---|---|
fetchShopKioskDomain() | Lấy domain hiện tại + base domain |
createShopKioskDomain(label) | Tạo subdomain lần đầu |
patchShopKioskDomain(label) | Cập nhật subdomain |
Hàm subdomainLabelFromFqdn(fqdn, base) extract phần label từ full domain. Ví dụ: mystore.kioskgaming.com → mystore. Preview URL được render live khi nhập.
Nếu server không config env var này, trang hiển thị warning amber và không cho phép tạo domain.
ShopCreditPurchaseModal.tsx — /purchase/checkout — Giao diện dark theme mua credits từ Agent/platform. Shop nhập số credits cần mua và chọn phương thức thanh toán.
- Credits × cost_rate = estimated charge (USD)
- Balance hiển thị: current, và new balance sau khi mua
- Rate hiển thị % bên cạnh estimated charge
- Input: chỉ nhận số nguyên (replace non-digits)
- Auto-refresh balance khi tab được focus lại sau >2 giây
- Recommend tab — 0x (zerox) deposit methods, tải dynamic từ API
- Other tab — các phương thức khác (cash app, v.v.)
- Search filter theo name/ticker/network/description
- Pagination:
PAYMENT_OPTIONS_PER_PAGEmỗi trang - Coming soon cards bị disable với opacity 50%
Validation theo phương thức
| Phương thức | Điều kiện |
|---|---|
zerox_* (0x deposit) |
Amount: $5 – $10,000 USD. Ngoài range → error message |
cashapp (ECash App) |
Chỉ nhận các amount trong ECASH_APP_ALLOWED_AMOUNTS cố định |
| Các phương thức khác | Minimum $10.00 USD (không có upper limit) |
Kết quả hiển thị: credit amount, amount to pay (currency formatted), transaction ID (order reference). Payment URL được nhúng trong iframe với sandbox permissions phù hợp. Nút "New purchase" reset form nhưng giữ modal mở.
OTP là cơ chế bảo mật cốt lõi cho mọi thao tác liên kết và chuyển player. Đảm bảo sự đồng ý của player trước khi thay đổi shop assignment.
Các trường hợp kích hoạt OTP
| Tình huống | OTP gửi đến | Mục đích |
|---|---|---|
Tạo player mới (not_found) |
Số phone nhập vào | Xác minh sở hữu phone |
Liên kết player vô chủ (unassigned) |
Phone của player | Đồng ý liên kết shop |
Chuyển từ shop khác (other_shop) |
Phone của player | Player xác nhận rời shop cũ |
Đặc điểm kỹ thuật OTP input
- Chỉ nhận số:
replace(/\D/g, '') - Giới hạn 6 ký tự:
.slice(0, 6) - Tracking widest font để dễ đọc
autocomplete="one-time-code"để browser autofillinputMode="numeric"để bàn phím số trên mobile- Resend OTP: cooldown timer qua
nextResendAt - Resend count tracking (
resendCount) - Verification method: SMS hoặc email (chọn khi tạo)
Khi player đã ở trong shop (in_shop), việc nạp credits chỉ cần qua confirmation modal — không cần OTP. OTP chỉ dùng cho các thao tác thay đổi shop assignment (create, link, transfer).
- Gửi bell notification đến player trong shop
- Tiêu đề tối đa 500 ký tự, body tối đa 1000 ký tự
- Chọn player (multi-select, chỉ
active) - Tùy chọn "Hide after" (datetime expiry)
- Preview real-time trước khi gửi
- Guard: kiểm tra
can_create_player_popuptừ dashboard data
- Gửi in-app popup đến player
- Display mode:
oncehoặcsticky_daily - Cùng guard
can_create_player_popup - Tiêu đề & body tương tự broadcast
- Tùy chọn expiresAt
- Multi-select player từ active list
SettingsPage.tsx — /settings — Chức năng duy nhất: cấu hình low balance alert.
- Toggle enable/disable cảnh báo
- Ngưỡng cảnh báo tối thiểu: $50 USD (
MIN_LOW_BALANCE_USD = 50) - Input step=0.01, min=$50
- Lưu qua
patchShopLowBalanceWarning({low_balance_warning_enabled, low_balance_threshold_usd}) - Load từ
fetchDashboard()— trườnglow_balance_warning_enabledvàlow_balance_threshold_usd
TransactionsPage.tsx — /transactions — Danh sách giao dịch ví shop. Hiển thị tiền vào/ra từ góc nhìn shop.
- Search: theo ID, title, counterparty, referenceId, metadata (JSON search)
- Method filter:
all/cash/card - Load 200 giao dịch mới nhất, sort desc
- Hook
useShopWalletTransactions
- Amount format:
+/−theo góc nhìn shop - Balance = số dư shop ngay sau giao dịch
- Helpers:
getPresentation(),normalizeMeta(),txCreatedAt() - Empty state khi không có giao dịch nào
ShopPaymentReturnPage.tsx — Ba trang trả về sau khi thanh toán qua provider bên ngoài.
| Route | Component | Trạng thái |
|---|---|---|
/payment/success |
ShopPaymentSuccessPage |
Thanh toán thành công |
/payment/cancelled |
ShopPaymentCancelledPage |
Người dùng hủy |
/payment/error |
ShopPaymentErrorPage |
Lỗi thanh toán |
Dependencies chính
Internal packages
Workspace packages: file:../packages/ui và file:../packages/page-loading
Cấu trúc thư mục
kioskgaming_shop/src/ ├── pages/ # 11 page components │ ├── DashboardPage.tsx │ ├── PlayersPage.tsx │ ├── PlayerDetailPage.tsx │ ├── KioskDomainPage.tsx │ ├── ShopCreditPurchaseModal.tsx │ ├── TransactionsPage.tsx │ ├── TransferToPlayerPage.tsx │ ├── SendBroadcastPage.tsx │ ├── SendPlayerPopupPage.tsx │ ├── SettingsPage.tsx │ └── ShopPaymentReturnPage.tsx ├── components/ │ ├── auth/ # LoginPage, ProtectedRoute │ ├── credits/ # ShopCreditPurchaseTrigger │ ├── dashboard/ # ShopDashboardOverview │ ├── home/ # ShopHomeRecentTransactions │ ├── layout/ # AppLayout │ ├── players/ # PlayerDetailPanel │ ├── ui/ # PortalEmptyState │ ├── ShopPlayerLinkByPhonePanel.tsx │ ├── ShopNotificationBell.tsx │ └── ShopInAppPopupGate.tsx ├── services/ │ └── api.ts # 30+ exported API functions ├── hooks/ │ ├── useAuth.tsx │ ├── useShopWalletTransactions.ts │ └── useSupportedZeroxMethods.ts ├── types/ # TypeScript type definitions ├── utils/ │ ├── phone.ts # isPlausibleInternationalPhone() │ └── shopWalletLedger.ts └── constants/ # dispatchShopCreditBalanceRefresh
Toàn bộ Routes
| Path | Component | Ghi chú |
|---|---|---|
/ | DashboardPage | Stat cards + recent transactions |
/players | PlayersPage | List + create/transfer modals |
/players/:playerId | PlayerDetailPage | Chi tiết player cụ thể |
/transactions | TransactionsPage | Wallet ledger 200 giao dịch |
/purchase/checkout | ShopCreditPurchaseModal | Dark UI, 0x payments + other |
/kiosk-domain | KioskDomainPage | Subdomain create/edit |
/settings | SettingsPage | Low balance alert settings |
/popups/send | SendPlayerPopupPage | Permission-gated |
/broadcasts/send | SendBroadcastPage | Bell notifications |
/transfer/legacy | TransferToPlayerPage | Legacy, được embed vào Players modals |
/transfer | — | Redirect → /players |
/purchase | — | Redirect → / |
/payment/success | ShopPaymentSuccessPage | Return từ payment provider |
/payment/cancelled | ShopPaymentCancelledPage | Return từ payment provider |
/payment/error | ShopPaymentErrorPage | Return từ payment provider |
/login | LoginPage | Auth không cần ProtectedRoute |
* | — | Catch-all redirect → / |
Patterns đáng chú ý
dispatchShopCreditBalanceRefresh() từ constants/ — emit custom DOM event để sync credit balance giữa components không có quan hệ cha-con trực tiếp.
ShopCreditPurchaseModal lắng nghe document.visibilitychange — nếu tab ẩn >2 giây rồi focus lại, tự động refresh balance (giả định người dùng vừa thanh toán ở tab khác).
Mỗi lần mở modal tạo/transfer player, key tăng lên 1 (setModalKey(k => k+1)). React unmount/remount component để clear toàn bộ state form.