B2B Portal React 18 · CRA + Craco Capacitor 7 (Android APK) TypeScript 4.9 kioskgaming_shop/

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.

25+
Protected routes
2
Wallet (Credit + USD)
/shop/*
API prefix
$50
Min low-balance alert
🏪
Store Role in the System

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.

1st floor
Super Agent
Manage the entire system, distribute credits to Agents
2nd floor
Agent
Provide credits for many Shops, set cost rate
3rd floor — HERE YOU ARE
Shop
Manage a kiosk location, link and load credits for Player
4th floor
Player
Customers play games at the store's kiosk
Shop Wallet

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.

Cost Rate from Agent

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.

Private Domain Kiosk

Each shop is configured with a separate subdomain attenShop.kioskgaming.com— Player accesses this URL to play the game at the store location.

📊
Dashboard — Store Overview

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
💡
Auto refresh after payment

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).

Components on Dashboard
  • ShopCreditPurchaseTrigger— button to open credit purchase with orange color tone
  • ShopDashboardOverview— shop information (name, email, status)
  • ShopHomeRecentTransactions— recent transaction history (appearance="dashboard")
👥
Player Management

PlayersPage.tsx— Player list, search, sorting, pagination. Two main modals: Create Player and Transfer from Another Shop.

Player list table
  • 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)
Two main action buttons
  • 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 usesShopPlayerLinkByPhonePanelwith propflowdifferent
  • Modal key is reset when reopened to clear the old state
  • Two flows can switch each other via callbackonSwitchFlow

ShopPlayerLinkByPhonePanel — Core player link logic

Component is common to both flows. Integrationreact-phone-number-inputto enter international phone numbers with country picker.

not_found
There are no accounts with this phone number in the system.
→ Send OTP to create & link — create a new player and link immediately
in_shop
Player belongs to this store. Credits can be loaded immediately.
→ Display phone on the board, no OTP required
unassigned
Player exists but has not been assigned to any shop.
→ Send OTP to link to this shop
other_shop
Player belongs to another shop. Transfer confirmation required.
→ Send OTP to transfer to this shop (player confirm)

OTP Registration Flow (Create Player)

Step 1
Enter phone
react-phone-number-input, international format
Step 2
Lookup
lookupPlayerByPhone() → returns status
Step 3
Initiate OTP
registerPlayerInitiate() → get registration_id
Step 4
Enter OTP
6 digits, numeric only, autocomplete="one-time-code"
Step 5
Verify
verifyPlayerRegistration() → player linked

Support OTP resend with cooldown timer (nextResendAt) andresendCounttracking. tracking. Verification method: SMS or email.

PlayerDetailPage — Player details

/players/:playerIdPlayerDetailPage.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
🔄
Transfer Player & Load Credits — TransferToPlayerPage
⚠️
Note about routes

/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.

OTP Transfer Flow (Transfer from another 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
Top up Credits for Player (in_shop)
  • 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

otpKindWhenInitialization APIAPI 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)
📱
Pre-fill from the Players table

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.

🌐
Kiosk Domain — Subdomain configuration

KioskDomainPage.tsx/kiosk-domain— Each store has its own kiosk URLtenShop.kioskgaming.com. This page allows creating or changing subdomains.

Three UI states

1. No domain yet (create new one)

Form with input "Subdomain label", live URL preview, "Create kiosk URL" button. Validation: only accept lowercase, numbers, hyphens.

2. Already have a domain (read-only)

Displays the current URL ashttps://preview. The "Edit" button (Pencil icon) switches to the editing form.

3. Editing domain

Subdomain change form with warning "Changing the URL may break old bookmarks until DNS updates propagate." There are Cancel and Save buttons.

API & Logic

JawDescribe
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.commystore. Preview URL is rendered live as entered.

⚠️
SHOP_KIOSK_BASE_DOMAIN

If the server does not configure this env var, the page displays an amber warning and does not allow domain creation.

💳
Buy Credits — ShopCreditPurchaseModal

ShopCreditPurchaseModal.tsx/purchase/checkout— Dark theme interface buys credits from Agent/platform. Shop enters the number of credits to buy and select payment method.

Calculate costs
  • 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
Two payment methods tabs
  • 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

MethodCondition
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)
Step 1
Enter credits
Integer, calculate estimated charge USD
Step 2
Select tabs
Recommend (0x) or Other
Step 3
Select method
PaymentMethodCard with icon, ticker
Step 4
Continue to payment
purchaseShopCredits() → get paymentUrl
Step 5
Pay
iframe embed + "Open in new tab" button
After successfully creating an order

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 security — Two-step authentication

OTP is the core security mechanism for all player connection and transfer operations. Ensure player consent before changing shop assignment.

Cases of OTP activation

SituationOTP sentPurpose
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 autofill
  • inputMode="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)
🔒
Top up credits without OTP

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 Notifications — Broadcast & Popup
SendBroadcastPage — /broadcasts/send
  • Send bell notifications to players in the shop
  • Title maximum 500 characters, body maximum 1000 characters
  • Select player (multi-select, onlyactive)
  • "Hide after" option (datetime expiry)
  • Preview real-time before sending
  • Guard: checkcan_create_player_popupfrom dashboard data
SendPlayerPopupPage — /popups/send
  • Send in-app popup to player
  • Display mode: onceorsticky_daily
  • Same guardcan_create_player_popup
  • Title & body is similar to broadcast
  • expiresAt option
  • Multi-select player from active list

⚙️
Settings — Low Balance Warning

SettingsPage.tsx/settings— Unique function: configure low balance alert.

Low Balance Warning
  • Toggle enable/disable warnings
  • Minimum warning threshold:$50 USD (MIN_LOW_BALANCE_USD = 50)
  • Input step=0.01, min=$50
  • Save itpatchShopLowBalanceWarning({low_balance_warning_enabled, low_balance_threshold_usd})
  • Load wordsfetchDashboard()- schoollow_balance_warning_enabledandlow_balance_threshold_usd

💰
Transactions — Transaction History

TransactionsPage.tsx/transactions— List of shop wallet transactions. Displays cash in/out from the shop's perspective.

Filter
  • Search: by ID, title, counterparty, referenceId, metadata (JSON search)
  • Method filter: all / cash / card
  • Load 200 latest transactions, sort desc
  • Hook useShopWalletTransactions
Show
  • Amount format: +/from the shop's perspective
  • Balance = shop balance immediately after transaction
  • Helpers: getPresentation(), normalizeMeta(), txCreatedAt()
  • Empty state when there are no transactions
↩️
Payment Return Pages

ShopPaymentReturnPage.tsx— Three pages returned after payment via external provider.

RouteComponentStatus
/payment/success ShopPaymentSuccessPage Payment successful
/payment/cancelled ShopPaymentCancelledPage User cancels
/payment/error ShopPaymentErrorPage Payment error
⚙️
Technical — React 18 · CRA + Craco

Main Dependencies

React 18.2 TypeScript 4.9 React Router v6.8 Tailwind CSS 3.2 react-hook-form 7.43 react-hot-toast 2.4 react-phone-number-input 3.4 axios 1.3 lucide-react 0.263 @marsidev/react-turnstile

Internal packages

@kioskgaming/ui @kioskgaming/page-loading

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

PathComponentNote
/DashboardPageStat cards + recent transactions
/playersPlayersPageList + create/transfer modals
/players/:playerIdPlayerDetailPageSpecific player details
/transactionsTransactionsPageWallet ledger 200 transactions
/purchase/checkoutShopCreditPurchaseModalDark UI, 0x payments + other
/kiosk-domainKioskDomainPageSubdomain create/edit
/settingsSettingsPageLow balance alert settings
/popups/sendSendPlayerPopupPagePermission-gated
/broadcasts/sendSendBroadcastPageBell notifications
/transfer/legacyTransferToPlayerPageLegacy, embedded into Players modals
/transferRedirect → /players
/purchaseRedirect → /
/payment/successShopPaymentSuccessPageReturn from payment provider
/payment/cancelledShopPaymentCancelledPageReturn from payment provider
/payment/errorShopPaymentErrorPageReturn from payment provider
/loginLoginPageAuth does not require ProtectedRoute
*Catch-all redirect → /

Notable Patterns

Custom Event Bus

dispatchShopCreditBalanceRefresh()fromconstants/— emit custom DOM event to sync credit balance between components that do not have a direct parent-child relationship.

visibilitychange refresh

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).

Modal Key Reset

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.