Shop Portal
(Store Management Portal)
B2B interface forGaming kiosk store owner. Manage players, cashier/shift, deposit/withdraw credits, online USD wallet, kiosk domain, counter deposits/redemptions — in the chain SuperAgent → Agent →Shop → Cashier → Player.
The shop is located on the middle level of the hierarchy. They buy credits from the superior Agent and distribute them to the Player at the actual business location.
Each shop has its own credit wallet. Credit purchased from Agent, then distributed to Player when they play the game. Balance displayed on Dashboard with low-balance warning.
Each shop receives onecost_rateset by Agent. For example, a rate of 0.85 means buying 100 credits will cost $85 USD. Rate is displayed on Dashboard card.
Each shop is configured with a separate subdomain attenShop.kioskgaming.com— Player accesses this URL to play the game at the store location.
DashboardPage.tsx— The home page displays 4 main stat cards and a list of recent transactions.
| Stat Card | Icon | Value | Type |
|---|---|---|---|
| Shop wallet | Coins |
Credit balance (2 decimal places) | purchase— open modal to buy credits |
| Players | Users |
Number of linked players | link → /players |
| Cost rate | Percent |
Percentage from Agent (eg: 85.00%) | plain— just watch |
| Kiosk URL | Globe |
"Setup" or current subdomain | link → /kiosk-domain |
Dashboard usedlocation.state.openCreditPurchaseto open the credits purchase modal when navigating from the ShopCreditPurchaseModal page. After placing an order, the dashboard automatically refreshes the balance in the background (silent mode, loading spinner is not displayed).
ShopCreditPurchaseTrigger— button to open credit purchase with orange color toneShopDashboardOverview— shop information (name, email, status)ShopHomeRecentTransactions— recent transaction history (appearance="dashboard")
PlayersPage.tsx— Player list, search, sorting, pagination. Two main modals: Create Player and Transfer from Another Shop.
- Responsive: card view on mobile, table view on desktop (≥md)
- Search: debounce 300ms, search by phone or email
- Sort:
shop_assigned_at(default, desc) orname - Paging: 10/20/50 rows, displays total number of players
- Click username →
/players/:playerId - Status badge:
active(emerald) or other (gray)
- Create player— Open a new creation modal or link a player without a shop (flow="create")
- Transfer from another shop— open the modal to transfer players from another shop to (flow="transfer")
- Each modal uses
ShopPlayerLinkByPhonePanelwith propflowdifferent - Modal key is reset when reopened to clear the old state
- Two flows can switch each other via callback
onSwitchFlow
ShopPlayerLinkByPhonePanel — Core player link logic
Component is common to both flows. Integrationreact-phone-number-inputto enter international phone numbers with country picker.
OTP Registration Flow (Create Player)
Support OTP resend with cooldown timer (nextResendAt) andresendCounttracking. tracking. Verification method: SMS or email.
PlayerDetailPage — Player details
/players/:playerId — PlayerDetailPage.tsx— View and manage each player. IntegrationPlayerDetailPanelcomponent for detailed UI.
- Load player data and shop credit balance in parallel
- Dispatch
shopCreditBalanceRefreshcustom event after each credit operation - 404 empty state if playerId is invalid or does not belong to the shop
- Breadcrumb back →
/players
/transferredirect to/players (legacy). Trang TransferToPlayerPage.tsxcurrently staying/transfer/legacy— this function is directly integrated into the PlayersPage modal viaShopPlayerLinkByPhonePanel.
Trang /transfer/legacyis a comprehensive screen: lookup player by phone, link/create player (OTP), and load credits for players already in the shop.
- Lookup phone → status =
other_shop initiatePlayerTransfer(playerId)→ receive transfer ID- OTP sent to player's phone (they must confirm)
verifyPlayerTransfer(transferId, otp)→ player switches to this shop- After verifying, lookup again to update the UI status
- Only works when player status =
in_shop - Enter amount (number input, step=any) and payment method
- Payment method:
cashorbank transfer - Confirmation modal with detailed table (balance before/after)
- Check for sufficient funds right on the form
postShopPlayerCredits(playerId, {action:'add', amount, payment_method})
Two types of OTP in TransferToPlayerPage
| otpKind | When | Initialization API | API verify |
|---|---|---|---|
create |
Player does not exist yet (not_found) or don't have a shop yet (unassigned) |
registerPlayerInitiate(phone) |
verifyPlayerRegistration(id, otp) |
assign |
Player is in another shop (other_shop) |
initiatePlayerTransfer(playerId) |
verifyPlayerTransfer(id, otp) |
When navigating to/transfer/legacywithlocation.state.prefillPhone, the page automatically fills in the phone number and runs a lookup immediately. Then clear the state to avoid re-filling when refreshing.
KioskDomainPage.tsx — /kiosk-domain— Each store has its own kiosk URLtenShop.kioskgaming.com. This page allows creating or changing subdomains.
Three UI states
Form with input "Subdomain label", live URL preview, "Create kiosk URL" button. Validation: only accept lowercase, numbers, hyphens.
Displays the current URL ashttps://preview. The "Edit" button (Pencil icon) switches to the editing form.
Subdomain change form with warning "Changing the URL may break old bookmarks until DNS updates propagate." There are Cancel and Save buttons.
API & Logic
| Jaw | Describe |
|---|---|
fetchShopKioskDomain() | Get current domain + base domain |
createShopKioskDomain(label) | Create subdomain for the first time |
patchShopKioskDomain(label) | Update subdomains |
JawsubdomainLabelFromFqdn(fqdn, base)extract the label part from the full domain. For example:mystore.kioskgaming.com → mystore. Preview URL is rendered live as entered.
If the server does not configure this env var, the page displays an amber warning and does not allow domain creation.
ShopCreditPurchaseModal.tsx — /purchase/checkout— Dark theme interface buys credits from Agent/platform. Shop enters the number of credits to buy and select payment method.
- Credits × cost_rate = estimated charge (USD)
- Balance displayed: current, and new balance after purchase
- Rate displays % next to estimated charge
- Input: only accepts integers (replace non-digits)
- Auto-refresh balance when tab is refocused after >2 seconds
- Recommend tab— 0x (zerox) deposit methods, dynamic loading from API
- Other tab— other methods (cash app, etc.)
- Search filter by name/ticker/network/description
- Pagination:
PAYMENT_OPTIONS_PER_PAGEper page - Coming soon cards are disabled at 50% opacity.
Validation by method
| Method | Condition |
|---|---|
zerox_* (0x deposit) |
Amount: $5 – $10,000 USD. Out of range → error message |
cashapp (ECash App) |
Only accept amounts inECASH_APP_ALLOWED_AMOUNTSpermanent |
| Other methods | Minimum $10.00 USD (no upper limit) |
Displayed results: credit amount, amount to pay (currency formatted), transaction ID (order reference). Payment URL embedded in iframe with appropriate sandbox permissions. The "New purchase" button resets the form but keeps the modal open.
OTP is the core security mechanism for all player connection and transfer operations. Ensure player consent before changing shop assignment.
Cases of OTP activation
| Situation | OTP sent | Purpose |
|---|---|---|
Create new player (not_found) |
Phone number entered | Verify phone ownership |
Link unassigned player (unassigned) |
Player phone | Agree to shop link |
Transfer from another shop (other_shop) |
Player phone | Player confirms leaving old shop |
OTP input specification
- Receive numbers only:
replace(/\D/g, '') - Limit 6 characters:
.slice(0, 6) - Tracking widest font for easy reading
autocomplete="one-time-code"let the browser autofillinputMode="numeric"to the numeric keypad on mobile- Resend OTP: cooldown timer via
nextResendAt - Resend count tracking (
resendCount) - Verification method: SMS or email (select when creating)
When the player is in the shop (in_shop), loading credits just goes throughconfirmation modal— no OTP needed. OTP is only used for shop assignment changes (create, link, transfer).
- Send bell notifications to players in the shop
- Title maximum 500 characters, body maximum 1000 characters
- Select player (multi-select, only
active) - "Hide after" option (datetime expiry)
- Preview real-time before sending
- Guard: check
can_create_player_popupfrom dashboard data
- Send in-app popup to player
- Display mode:
onceorsticky_daily - Same guard
can_create_player_popup - Title & body is similar to broadcast
- expiresAt option
- Multi-select player from active list
SettingsPage.tsx — /settings— Unique function: configure low balance alert.
- Toggle enable/disable warnings
- Minimum warning threshold:$50 USD (
MIN_LOW_BALANCE_USD = 50) - Input step=0.01, min=$50
- Save it
patchShopLowBalanceWarning({low_balance_warning_enabled, low_balance_threshold_usd}) - Load words
fetchDashboard()- schoollow_balance_warning_enabledandlow_balance_threshold_usd
TransactionsPage.tsx — /transactions— List of shop wallet transactions. Displays cash in/out from the shop's perspective.
- Search: by ID, title, counterparty, referenceId, metadata (JSON search)
- Method filter:
all/cash/card - Load 200 latest transactions, sort desc
- Hook
useShopWalletTransactions
- Amount format:
+/−from the shop's perspective - Balance = shop balance immediately after transaction
- Helpers:
getPresentation(),normalizeMeta(),txCreatedAt() - Empty state when there are no transactions
ShopPaymentReturnPage.tsx— Three pages returned after payment via external provider.
| Route | Component | Status |
|---|---|---|
/payment/success |
ShopPaymentSuccessPage |
Payment successful |
/payment/cancelled |
ShopPaymentCancelledPage |
User cancels |
/payment/error |
ShopPaymentErrorPage |
Payment error |
Main Dependencies
Internal packages
Workspace packages: file:../packages/uiandfile:../packages/page-loading
Directory structure
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
Complete Routes
| Path | Component | Note |
|---|---|---|
/ | DashboardPage | Stat cards + recent transactions |
/players | PlayersPage | List + create/transfer modals |
/players/:playerId | PlayerDetailPage | Specific player details |
/transactions | TransactionsPage | Wallet ledger 200 transactions |
/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, embedded into Players modals |
/transfer | — | Redirect → /players |
/purchase | — | Redirect → / |
/payment/success | ShopPaymentSuccessPage | Return from payment provider |
/payment/cancelled | ShopPaymentCancelledPage | Return from payment provider |
/payment/error | ShopPaymentErrorPage | Return from payment provider |
/login | LoginPage | Auth does not require ProtectedRoute |
* | — | Catch-all redirect → / |
Notable Patterns
dispatchShopCreditBalanceRefresh()fromconstants/— emit custom DOM event to sync credit balance between components that do not have a direct parent-child relationship.
ShopCreditPurchaseModal listensdocument.visibilitychange— if the tab is hidden for >2 seconds and then focused again, the balance will automatically refresh (assuming the user has just paid in another tab).
Each time the create/transfer player modal is opened, the key increases by 1 (setModalKey(k => k+1)). React unmount/remount component to clear the entire state form.