Private rooms were 100% client-simulated (the "friend" auto-accepted then bots
filled invited seats). Now they're server-authoritative over SignalR:
Server (GameManager.PrivateRooms + GameHub):
- Room registry with create/invite/accept/decline/addBot/clearSeat/start/leave.
- Invite pushes a `roomInvite` to that user (Clients.User); the seat stays
"invited" (a pending guest with their real profile, resolved server-side) — it
is NEVER replaced by a bot.
- StartPrivate refuses while any invite is pending; only EMPTY seats fill with
bots. Then it spins up a live GameRoom and matchFound → both devices enter.
- Host leave / disconnect closes the room (roomClosed); members free their seat.
Client:
- signalr-service implements the room methods over the hub (+ room/roomInvite/
roomClosed events, room mapping, onRoomInvite); mock keeps offline no-ops.
- online-store accept/declineInvite; RoomScreen blocks "Start" while an invite
is pending and auto-enters the live game on matchFound (host + friend).
- New global InviteModal (accept/decline) + i18n (invite.*, room.waitAccept).
Addresses: (1) no bot replacement, (2) game waits for acceptance, (3) invited
friend shown as a pending guest with their name/avatar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- sound.ts: restored startMusic (was a no-op stub) playing the selected track
through musicGain (in-game mute still applies); default track switched to
"playful" (per user). Music auto-starts on init when enabled.
- Profile → Audio: re-added a music on/off toggle (so players can disable it
outside the game too); SFX toggle unchanged.
- Economy tune: starting coins 1000 → 2000 (mock defaultProfile + server
ProfileDto) so new players start a bit richer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old reward formula rounded (40+6g)/50 on the raw goal, so adjacent
milestones could pay the same (e.g. "1 win" and "5 wins" both 50) and the curve
was lumpy — not a standard escalating-reward ladder. Reward now scales by tier
index: 50, 150, 250 … capped at 1500, strictly increasing per milestone.
Mirrored client (gamification.ts) + server (Gamification.cs) so live grants match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Shop: when short on coins the detail sheet now shows "{n} more coins" + a
"Get coins" CTA that opens the buy-coins page (was a dead disabled button).
- Chat: pin/unpin conversations (max 3, persisted to localStorage); pinned float
to the top with a gold pin. i18n chat.pin/unpin/pinLimit.
- Surrender: server now rate-limits forfeit asks at a human teammate
(45s per-user cooldown) so it can't be spammed. (Bot teammate still ends
immediately; teammate confirm dialog already existed.)
- OTP login hardening: Kavenegar send now parses the API status from the body
(HTTP 200 can still be a failure) + logs it + 12s timeout; client auth fetch
gets a 20s AbortController timeout so a lost response surfaces an error
instead of freezing on "sending…".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Master SVGs + generator in scripts/icon/ (icon.svg full design, icon-foreground.svg
cards-only for the Android adaptive layer, gen-icons.mjs via sharp).
- Web/PWA: regenerated favicon.ico (16/32/48), src/app/apple-icon.png, public/icon.svg,
icon-192/512, icon-maskable-512; manifest now lists png + maskable icons.
- Android (Capacitor): ic_launcher / ic_launcher_round / ic_launcher_foreground for all
densities + ic_launcher-playstore 512; adaptive background switched from flat white
to a navy radial-gradient drawable (matches the icon), foreground = the gold card fan.
Design: navy field, gold rounded frame, three fanned cards with a gold spade —
on-brand with the in-app "Persian luxury" look.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auth / security
- Rate-limit real SMS OTP sends (dev mode unlimited): 60s resend cooldown,
5 per phone/hour, 300/hour global backstop. OtpService.CheckAndRecordRate;
POST /api/auth/otp/request returns 429 {error,retryAfter}; AuthScreen shows
auth.rateLimited. Knobs in appsettings Sms (Sms__* env).
Private rooms (invite)
- Cancel-invite button on pending seats; friend picker shows presence
(online/offline/in-game, sorted online-first) and flags in-game players.
- Mock invite stays pending ~3.5s and a cancel truly stops the auto-accept
(was a bug that re-seated cancelled invites).
In-game UI
- Scoreboard is compact + shrink-safe (no overflow on narrow screens).
- Played trick cards land dead-center (were ~2px off the corner anchor).
Plus the in-flight typing-indicator work (GameHub, ChatScreen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mock service intentionally KEPT the persisted profile (hokm.profile) on
signOut, and getProfile() reloads it — so after logout the previous user's
name/gender/avatar resurrected from localStorage. Now signOut clears the
in-memory + persisted profile, and the SignalR service also clears its mock
fallback so the post-logout guest profile is fresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GameTable reactions button (and its tray) moved up from the bottom-right so it
no longer overlaps the player's cards on mobile portrait.
- Gender options are now Male / Female / Unknown — removed "other" from the
Gender type, GENDER_META, and the profile picker; the empty value renders as
«نامشخص» / "Unknown".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AuthScreen gated the code-entry step on devCode != null, so with real SMS
(no devCode) it got stuck after "send". Gate on a `sent` flag instead; add
sending state, send-failure message, "code sent" hint, change-number, and
raise the code input cap to 6 (codes are 5 digits).
- signOut now resets the store to a fresh guest profile, and the SignalR
service clears its cachedProfile — so the previous user's name/avatar no
longer linger after logout.
- i18n: auth.sending / sendFailed / codeSent / invalidPhone / changeNumber.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OtpService: generates a 5-digit code, stores it (in-memory, 120s TTL, max 5
tries, single-use), and sends it via Kavenegar verify/lookup
(template "hokmotp", %token = code). Normalizes +98/98 → 09xxxxxxxxx.
- /api/auth/otp/request + /verify now use it. No SMS_API_KEY ⇒ dev mode
(accepts a fixed code, returns devCode for local testing).
- Config: Sms section (appsettings) + Sms__* compose mapping + SMS_* in the
ENV_FILE template.
Security: sanitized deploy/ENV_FILE.example back to placeholders (it had picked
up real secrets) and added /deploy/ENV_FILE.local to .gitignore as the real
master copy (never committed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Myket's server-to-server validation is POST
/api/partners/applications/{pkg}/purchases/products/{sku}/verify with the
purchase token in the JSON body ({"tokenId": ...}) + X-Access-Token header —
not a GET with the token in the path. purchaseState 0 = valid.
Ref: https://myket.ir/kb/pages/server-to-server-payment-validation-api/
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements real Myket IAB for the Capacitor app (Myket has no purchase
deep-link like Bazaar — it uses the classic Google Play IAB v3 AIDL bound to
the Myket app):
- AIDL: com.android.vending.billing.IInAppBillingService (Myket-compatible).
- MyketBillingPlugin (Capacitor): binds ir.mservices.market via
"ir.mservices.market.InAppBillingService.BIND", runs getBuyIntent →
startIntentSenderForResult, verifies INAPP_DATA_SIGNATURE with the RSA key
(Security.java, SHA1withRSA), returns the purchaseToken; consume() too.
- MainActivity registers the plugin + forwards the purchase activity result.
- Manifest: ir.mservices.market.BILLING permission + <queries> for Android 11+
package visibility.
- build.gradle: enable buildFeatures.aidl (AGP 8 disables it by default).
- storeBilling: Myket goes through the plugin (RSA key embedded); after server
verify, BuyCoins consumes the purchase so coins can be re-bought.
Bazaar (deep-link) and web (ZarinPal) paths unchanged. Needs on-device testing
with the Myket app installed + published products.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Payment provider is baked at build time via NEXT_PUBLIC_STORE and selected by
storeBilling.getStore(). Add cross-env + flavor build scripts:
npm run build:web | build:bazaar | build:myket # web bundle per flavor
npm run cap:bazaar | cap:myket # build flavor + cap sync
npm run aab:bazaar | aab:myket # signed AAB (bundleRelease)
npm run apk:bazaar | apk:myket # release APK (assembleRelease)
web → ZarinPal gateway, bazaar → Cafe Bazaar IAB (deep-link), myket → Myket IAB
(native bridge). A plain native build with no flavor still falls back to bazaar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pack ids now equal the Bazaar/Myket SKUs (Coin5K / Coin12K / Coin28K /
Coin65K) in both the server source-of-truth (ProfileService.Packs) and the
mock, so the IAB credit path (Id == ProductId) grants the right coins.
Coin totals + prices already matched the registered products.
- storeBilling.getStore(): when running inside the Capacitor Android shell and
no explicit NEXT_PUBLIC_STORE flavor is set, default to "bazaar" so the APK
uses Cafe Bazaar IAB (the web build stays on the ZarinPal gateway). Myket's
native bridge still overrides when present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/promo.js builds an animated portrait promo from the store screenshots
(branded slides + Persian captions + Ken Burns), records it with Playwright,
and is encoded to store-assets/promo.mp4 via the system ffmpeg. Output dir
gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove RotatePrompt (the "rotate to landscape" overlay) — the app is portrait
now, so it only blocked the UI.
- page.tsx: best-effort orientation lock switched landscape → portrait.
- Add Playwright-based store-screenshot + icon scripts (scripts/shots.js,
game.js, icon.js); generated images are gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- storeBilling.ts and IabService PackageName defaulted to com.bargevasat.hokm,
but the real app id is com.bargevasat.app (capacitor + android applicationId).
The mismatch would break Bazaar deep-link purchases and server validation.
- Add IabOptions.BazaarRsaPublicKey to hold the Bazaar in-app billing RSA public
key (documented; for the Poolakey local-signature flow, unused by the current
deep-link + server pardakht verification).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Lock the app to portrait: AndroidManifest screenOrientation="portrait" and PWA
manifest orientation "portrait".
- GameTable felt now occupies the middle band (between top HUD and the hand) with
portrait proportions (w<=560, tall) so the you/partner/opponents diamond fits a
tall screen comfortably instead of a wide landscape ellipse.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- NavRail: one rounded "pill" tab bar on every screen (matches home). ScreenShell
lays out as a portrait column and floats the nav with margins + safe-area;
dropped the landscape side-rail variant.
- Home: the three mode cards now stack vertically as full-width rows (portrait
friendly) instead of a 3-up landscape row.
- Disconnect: removed the simulated random opponent "disconnect" in local games
(DISCONNECT_CHANCE) and the in-game DisconnectBanner — bots/filled seats just
auto-play their turn; no message, no pause. (Live reconnect grace still tracked
internally but no longer shows a banner.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Release signing via android/keystore.properties (git-ignored); build.gradle
signs release builds when the props file is present, stays unsigned otherwise.
- android/mirror-init.gradle: injects the myket.ir Maven mirror into every
project's buildscript (dl.google.com is unreachable here) and pins Build-Tools
to the installed 36.0.0. Build with:
gradlew assembleRelease bundleRelease -I mirror-init.gradle
(JAVA_HOME must point at a JDK 21 — Capacitor 8 compiles against Java 21.)
- gitignore keystores, keystore.properties, and /dist artifacts.
- Native-app feel: kill tap-highlight, long-press callout, and stray text
selection (inputs/messages opt back in); touch-action: manipulation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fullscreen on mobile:
- Android (Capacitor): MainActivity now runs edge-to-edge and hides the status +
navigation bars (immersive, transient-on-swipe), re-asserted on focus.
- PWA: manifest display -> "fullscreen" with display_override fallback chain;
viewport gains viewport-fit: cover for proper safe-area/edge-to-edge handling.
Moderation auto-hide:
- ProfileService.ReportUser now de-dupes nudity reports per reporter and, once
NudityHideThreshold (3) distinct players flag a target's avatar as nudity,
auto-removes their custom photo (reverts to default avatar). Counted from the
ledger, so still no schema change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Photo upload:
- Lower the custom profile-photo gate from level 25 to level 3 (client const +
i18n hint + server gate in ProfileService.Update). The level-25 "Expert" title
is unrelated and unchanged.
Report a player:
- New ReportReason type + service.reportUser(targetId, reason, details?).
- Report entry points: a "گزارش تخلف" button + reason picker (nudity / insult /
other) in the public-profile modal, and a flag button in the chat header
(reports the peer for an insulting chat) with a confirmation toast.
- Mock records reports to localStorage; SignalR POSTs /api/report.
- Server: POST /api/report → ProfileService.ReportUser stores the report in the
write-only ledger (kind="report", ref="{targetId}|{reason}|{details}") so no
schema change is needed (server uses EnsureCreated, not migrations).
- i18n: report.* keys (fa + en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both the mock and the .NET server already waited then bot-filled, but used a
random 12-18s window. Make it exactly 15s on both sides so the rule is clear:
wait 15s for real online players to join, then replace any unfilled seats with
bots and start.
- client: new MATCH_QUEUE_WAIT_MS = 15000 in gamification.ts; mock beginSearch
uses it instead of randInt(12000,18000).
- server: GameManager QueueWaitMs = 15000 (was randomized 12-18s per ticket).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New searchable city picker (src/lib/iran-cities.ts, ~60 Iranian cities,
fa/en search) shown as a gold reward card at the top of the profile Basic tab.
- First time a non-empty city is set, the player earns 500 coins (CITY_REWARD),
granted server-authoritatively. Collapses to a compact summary afterwards with
a "change city" option (no re-reward).
- Frontend: UserProfile.city + cityRewardClaimed; mock-service grants on first
set; session/service updateProfile accept `city`; celebratory toast + sfx.
- Backend (.NET): ProfileDto.City/CityRewardClaimed (JSON blob → no migration);
ProfileService.Update grants +500 once and writes a "city" ledger entry.
- i18n: city.* keys (fa + en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OnlineLobbyScreen: each league row is now a tappable play button (queues a
ranked match at that league's stake) with a forward arrow; the cheapest
enterable league is highlighted gold. Drops the redundant separate "ranked
random" CTA and the select-then-play step.
- Remove the background-music feature entirely: deleted the floating MusicToggle,
the TopBar music button, and the Profile audio music toggle + style picker.
sound.startMusic() is now an inert no-op so music never plays (sfx unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PostMatchRewardsModal: short-height (landscape) compaction so the win/forfeit
result fits without overflow (smaller emoji/coins/padding, max-h 94dvh, wider).
- Chat: emoji/sticker picker (owned reactions) — tap to send; hidden on focus.
- Unread messages: online-store now tracks a total `unread` (from
listConversations); NavRail Friends icon shows a badge (unread + requests),
refreshed every 12s on every screen; Friends «پیامها» tab badged too.
(Per-conversation unread badges already existed.)
- Remove "XP گران است" / "XP is expensive" from shop.xpHint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Room: teams side-by-side in landscape so all 4 seats fit (still scrolls).
- Achievements: rename the 5 rating tiers from «لیگ» (league) to «رتبه» (rank)
+ category «رتبه» — so "league" only means the 3 playable match leagues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Lobby: remove private-room CTA (it's on Home now) → fits without scroll.
- Home: private rooms now cost 150 coins/player (stake 150).
- Buy Coins: drop the "secure payment" note; redesign packs as game-shop coin
boxes (coin pile + amount + gold buy-price CTA), 2/3/4-col responsive.
- Notifications: minimal single-line corner toast, explicit ✕ close, hidden
during play so it never disturbs the game.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- NavRail: vertically center items in the side rail (was top-aligned).
- ScreenHeader: showXp defaults off — the level/XP bar no longer clutters every
sub-page (it lives on Home's chip + the Profile page).
- Shop: category tabs (avatars / fronts / backs / reactions / stickers / titles
/ XP) so only one category shows at a time — no more endless scroll.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Home screen wasn't centered like the sub-pages, so on desktop/tablet its
content drifted to the RTL start edge with dead felt on the left. Wrap it in a
centered max-w-3xl/landscape:max-w-5xl stage, vertically center the mode cards,
and size them up for tablet/desktop (min-h + larger max-w).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make all menu screens use the width on desktop/landscape and the UNO panels:
- Shop item grid 3→up to 6 cols; BuyCoins packs 2→4 cols on lg.
- Lobby: panel league pick (2-col) + 2-col CTA buttons.
- Achievements / Notifications / Leaderboard / Friends lists → responsive
grids (1 col mobile, 2 cols on lg); glass→panel on section containers.
- Chat: centered max-w-3xl column on desktop, green send button.
All responsive for mobile + desktop. tsc + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild HomeScreen to UNO's home layout: top bar (avatar+coins) + 3 big glossy
3D mode cards in the center (Online[gold,live-count badge] / vs-Computer[teal] /
Private Room[violet]) + a bottom icon nav bar (NavRail bottom variant, drops the
redundant home item). Speed toggle + language sit in a slim controls row. Online
card shows live player count; room card creates a private room then enters it.
New menu.room/menu.roomDesc i18n.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Theme: retint to lit emerald card-table felt + gold (body radial felt, green
glass panels). New component kit in globals.css: glossy chunky 3D btn-gold +
btn-green, .panel, .ribbon. Card backs pinned to classic navy.
- Home fully redesigned UNO-style: nav rail + branding + two big 3D play
buttons (gold online / green-glass vs-computer) + speed toggle; dropped the
redundant 4 tiles (the rail covers them). Fits landscape (short: variants).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild ScreenShell into a UNO-style app shell: a persistent NavRail (vertical
side rail in landscape, bottom tab bar in portrait — Home/Profile/Shop/Friends/
Leaderboard/Achievements, active highlighted gold) + a content panel that owns
its own scroll so the page never scrolls as a whole and uses the width in
landscape. Reskins all 10 menu screens at once. Transient screens (auth,
matchmaking, room) opt out via hideNav. New nav.home i18n key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: a landscape phone is wide (>=640px) but short, so width-based sm:
roominess inflated the title/buttons while the screen height was small -> the
right column overflowed (vs-Computer card cut off). Add a height-based
`short:` variant (@media max-height:520px) and compact Home's branding +
action cards under it so the column fits short landscape viewports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the web-app manifest link + manifest.ts route so bargevasat.ir no longer
triggers an "install/add to home screen" prompt. It's a plain marketing/SEO
site now. Only the game app (app.bargevasat.ir) remains a PWA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move orientation lock + RotatePrompt to app root → whole app is landscape-
first now (UNO-style), not just the game. Generalized rotate copy.
- Home: portrait unchanged; in landscape it becomes a 2-column app layout
(col A = branding + play actions, col B = tiles + footer) that fits the
short height with no scroll (landscape: Tailwind variants, overflow-hidden).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hokm plays best wide (UNO-style). On phones held in portrait, the game screen
shows a "rotate your phone" overlay (with a play-anyway escape hatch so OS
rotation-lock can't trap anyone). Best-effort screen.orientation.lock('landscape')
on Android/PWA; iOS/desktop reject it harmlessly. i18n rotate.* (fa+en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fixed-position music button covered content on mobile. Removed it from
the global overlay; mute now lives in the TopBar icon group (Home), and the
in-game HUD + Profile settings already have their own audio controls.
Tightened the TopBar icon row (p-1.5, gap-1, profile max-w-44%) so the extra
button still fits 360px phones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Home: logo and «برگ وسط» now sit on one row (prevents overflow), with
«بازی حکم آنلاین» as a small subtitle beneath the title next to the logo.
- Profile: add a خروج (Sign Out) button at the bottom (when signed in).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Profile chip: replace cramped fixed-width (w-[88px]) 3-line stack with a
clean 2-line layout (name on top; level · xp-bar · % on a flexing row),
capped at max-w-[46%] so it never crowds the icon group or overflows.
- Hero "Play online" title text-2xl→text-xl on phones (sm:text-2xl), truncate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Docker build 404'd on @types/react@19.2.17 (not in the Nexus mirror; npmjs
is blocked upstream). Pin shared deps to the exact versions the main app uses
(@types/react 19.2.16, etc.) and regenerate package-lock.json against the Nexus
registry so every resolved tarball is one Nexus can serve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Logo/title/hero/tiles/footer spacing now scales down on small screens
(sm: breakpoints) so the menu fits common phone viewports — the sign-in/
language footer was being pushed below the fold.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MatchmakingScreen: the 4 fixed w-16 slots (~292px) overflowed 320px phones;
now grid-fluid (w-full, gap-2 sm:gap-3, max-w-xs) so they always fit.
- ProfileScreen avatar picker now renders <Avatar id> (god/legend medallions)
instead of raw emoji — consistent with the displayed avatar and shop.
Swept Achievements/Leaderboard/BuyCoins/Auth/Shop/Profile/Lobby/Room — already
responsive (ScreenShell + min-w-0/truncate/shrink-0 throughout); no other
overflow found.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GameTable Backdrop (Hakem/Trump/Round/Match-over): scroll when taller than the
viewport via overflow-y-auto + min-h-full centering — no more clipped panels.
- DailyRewardModal: cap height + scroll (was overflow-hidden, clipped the 7-day grid).
- PostMatchRewardsModal: max-h uses dvh (mobile chrome safe).
- ScreenShell: add overflow-x-hidden so a too-wide child can't scroll horizontally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>