ZarinPal only accepts callbacks on pay.flatrender.ir, so bargevasat
pays through the shared broker and is credited via a signed webhook.
- FlatPayService: broker client (HMAC-signed /v1/pay/request) + webhook
signature verification + in-memory idempotency guard.
- Program.cs: /api/coins/pay/request prefers the broker when configured
(FlatPay__ApiKey/Secret set), else the legacy direct ZarinPal path;
new public POST /api/coins/pay/webhook verifies the HMAC and credits
coins from the echoed metadata (idempotent).
- appsettings + docker-compose: FlatPay config (empty ⇒ legacy path).
- web: recognise the broker's ?status=Paid return + re-refresh profile
(coins are credited server-side via webhook).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
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>
Each notification now navigates to its related screen when tapped (toast or
list): friend_request/invite -> Friends, achievement/reward -> Achievements,
daily -> opens the daily-reward modal, coin-purchase success -> Shop. An
explicit per-notification 'route' overrides the kind default.
List rows are swipeable (drag aside) and have an X to dismiss individually,
plus a Clear-all button; the toast can be flicked up to dismiss or tapped to
open. New store actions: markRead/remove/clearAll + openNotification navigator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Trick area: smaller offsets (±50/52) + retuned scale so the played pile sits
centered in the felt instead of flung out to the side seats.
- ErrorBoundary around screens + overlays: a render error now shows a recoverable
in-app message with the cause (and logs componentStack) instead of the browser's
blank "page couldn't load" — helps pinpoint the post-purchase crash.
Verified: tsc + next build clean; web rebuilt on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The "This page couldn't load" after a redeploy was a stale bundle: a tab open
across a deploy requests JS chunks that no longer exist (ChunkLoadError). Added
a global error/unhandledrejection guard that reloads once to fetch the fresh
bundle (sessionStorage-guarded against loops, cleared after a healthy run).
- Reaction tray width → w-[min(270px,86vw)] so it never overflows narrow phones.
Verified: tsc + next build pass; web image rebuilt on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MusicToggle: global floating button (enable/disable music from any screen;
hidden on the table, which has its own audio control in its HUD). Uses
sound-store toggleMusic.
- Card sounds now use a synthesized card-draw "swish" (filtered noise burst with
a downward sweep) for cardPlay (+ soft landing tap) and deal (a flurry),
replacing the old beep tones.
Verified: tsc + next build pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New global celebration system: celebration-store (queue) + CelebrationOverlay
(animated: count-up XP, filling bar, level-up pop, achievement cards; plays
levelUp/award sounds; tap or auto-dismiss). Rendered in page.tsx.
- Shop: every purchase now celebrates — XP packs animate XP gain + level-up,
cosmetics show a "purchased!" pop. Newly-unlocked achievements (diffed from
the profile before/after) animate too.
- XP purchases now actually evaluate achievements: gamification.evaluateAchievements
(client) + Gamification.EvaluateAchievements (server, called in ShopBuy xp path)
unlock level milestones + grant their coins.
Verified live: buying XP took L1→L5, unlocked level_5 server-side and credited its
reward. tsc + dotnet + next build clean; images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keep the Persian-luxury palette, adopt casual card-game UX:
- New .press-3d primitive (solid underside that compresses on tap, reduced-motion
aware) for tactile buttons.
- Home: a large glowing hero "Play online" button (Play icon + chevron), a
tactile vs-computer card, and chunkier mode tiles with color-tinted icon chips
(teal/sky/gold/rose). All handlers + i18n unchanged.
Verified: tsc + next build clean; web rebuilt :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grounded in the two installed design skills:
- Safe-area insets (notch/home-bar): .safe-top/.safe-bottom/.safe-x helpers
applied to the game-table HUD + bottom hand + reaction button, and to
ScreenShell + HomeScreen (covers Profile/Shop/Leaderboard/etc.).
- Touch targets ≥44px: table HUD buttons (mute/forfeit/exit) and the reaction
button now meet the 44/48px minimum.
- HUD readability: seat name/level labels (which float over the felt) get a
text-shadow (.hud-shadow) and stronger contrast.
Verified: tsc + next build clean; web image rebuilt on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- XP packs in the store (coin-priced, intentionally expensive): xp1 200/5k,
xp2 600/12k, xp3 1500/25k. Consumable (grant XP, can level up) — server
ShopBuy handles kind "xp" via an authoritative XpPacks map + Gamification.GrantXp;
mock mirrors. New shop section + shop.xp/xpHint i18n.
- Every game grants XP and the WINNER earns 2x: matchXp is now
base*(won?2:1)*leagueFactor (was a flat +80 win bonus). Mirrored server-side.
- Premium (pro) perks: 1.5x XP multiplier (applied in applyMatchResult /
ApplyMatch by plan), plus animated shimmering gold chat bubbles for your own
messages (premium-chat CSS; ChatScreen gates on plan).
Verified: tsc + next + dotnet build clean; sim passes; live server — buying xp2
took L1→L3 and deducted 12k coins under the new curve. Images rebuilt :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Achievements (client + server mirror, metric-driven so the list is one source):
- 37 achievements across 6 categories (Victories, Kot, Streaks, Levels, Ranks,
Veterancy) incl. 7–0 sweeps, kot milestones (1/5/10/25/50/100), win streaks
(3/5/10/15), level milestones every 5 (5..50), rank floors, games/tricks.
- New AchievementsScreen with category tabs, progress bars, coin + sticker-unlock
badges, and unlocked/locked states; summary header (unlocked count + coins).
- Some achievements unlock sticker packs: Seven–Zip→Hokm, 25 Kots→Taunts,
100 Wins→Persian (ownedStickerPackIds now also honors profile.unlocked).
- Prestige titles added: Expert, Professional, Captain, Leader (+ existing).
- Tracks new stat shutoutWins; MatchSummary.shutout (7–0). Profile shows a
6-item preview + "view all" link.
Leagues: 3 ranked entry tiers — Starter (100, lvl1), Pro (500, lvl10),
Expert (1000, lvl20). Higher league stakes more, so wins/losses swing bigger;
kot bonus now scales to the stake (40%). OnlineLobby shows league cards with
level gating.
Profile photo upload gated to level 25 (client button + server Update guard).
Win animation: PostMatchRewardsModal now shows an animated coins-won count-up
hero on a win.
Verified: dotnet build + tsc + next build clean; sim unlocks 26 achievements
over 500 matches; live server grants first_win/first_kot/shutout_1 and pays
2050 coins on an expert-league shutout+kot win. Images rebuilt on :1500/:1505.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Leaving the table (back button, browser/hardware back) no longer resets the
game — it minimizes it and stays resumable:
- game-store: add `paused` + minimize()/resume(). Single-player (AI) matches
pause their local timers so nothing happens while away; live (server-run)
matches keep streaming via the still-active subscription (the .NET GameRoom
already runs the match to completion and re-broadcasts state on reconnect).
- GameScreen: an unmount effect minimizes any in-progress match no matter how
you leave; only a finished match (reward dismissed) tears down.
- ResumeGameBar: floating "return to game" pill shown from any screen while a
match is alive, or while a finished match still has an unseen reward.
- page.tsx: after a full reload, re-enter live mode (minimized) when the server
re-broadcasts state, and notify when a match you left finishes while away.
Verified: tsc + next build clean; web image rebuilt and serving on :1500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pipeline (.gitea/workflows/ci-cd.yml), all images/packages via Nexus mirror:
- CI api-build: dotnet restore/build server/Hokm.slnx + run Hokm.Sim (rules).
- CI web-check: npm install + tsc --noEmit + next build (static export).
- deploy (self-hosted): pre-deploy pg_dump backup, rollback image tag, build,
bring up db -> server -> web with stop+rm+up --no-deps (no force-recreate,
no bare compose down), health-wait each, prune.
Local stack (docker-compose.yml), ports in 1500-1600 so it coexists with manual
dev on 3000/5005: web :1500 (nginx static) -> server :1505 (.NET) -> db :1510
(postgres, named volume + backups). Dockerfiles: server (.NET, NuGet via
nuget.docker.config, binds 0.0.0.0, busybox wget healthcheck) + web (Next static
export -> nginx, NEXT_PUBLIC_* baked as build args). nginx.conf SPA fallback.
Config: server CORS is now config-driven (Cors__Origins) so the deployed web
origin is allowed without code edits. deploy/ENV_FILE.example documents the
Gitea ENV_FILE secret; DEPLOY.md covers setup/run/LAN-IP/rollback/migrations.
Fonts: switch Vazirmatn + Plus Jakarta Sans from next/font/google (build-time
Google fetch -> fails on the Iran CI runner) to self-hosted @fontsource-variable
packages. Build is offline and ~3x faster; 7 woff2 emitted into out/.
Verified locally: dotnet build slnx + Hokm.Sim (300 matches, exit 0); tsc clean;
next build clean with self-hosted fonts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ZarinpalService (request/verify) + /api/coins/pay/request (JWT) and
/api/coins/pay/callback (verify → credit via ProfileService.BuyCoins → redirect
back with ?pay=success); merchant id from config (sandbox default)
- Client buyCoins (live) returns the StartPay redirect URL; BuyCoinsScreen
redirects; page.tsx handles the ?pay return (notify + refresh)
- Verified: sandbox request returns a real StartPay URL
- Documented Cafe Bazaar (Poolakey) / Myket IAB as the required store payment path
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Coins only matter for ranked: free games (vs computer / private friend rooms)
cost nothing; random ranked requires an entry (stake), gated by balance →
routes to buy-coins when short
- Buy Coins page (CoinPack/getCoinPacks/buyCoins; mock credits now, real
Zarinpal/IDPay TODO); TopBar coins → buy; lobby create-room is Free
- Friends: removed instant red ✕ delete; UserMinus → inline confirm before remove
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PlayerHand measures viewport and compresses the fan (responsive card size +
dynamic overlap) so all 13 cards are visible and tappable on phones — no
off-screen cards, no horizontal scroll; added tap feedback
- globals: html/body overflow-x hidden + overscroll-behavior none
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Next static export (output: export) wrapped by Capacitor; appId
com.bargevasat.app, appName «برگ وسط»
- android/ native project + @capacitor/app; hardware back handled by
CapacitorBack (back a screen, exit at home)
- npm scripts (cap:sync, android:open, android:apk), ANDROID.md
- Gradle Maven-mirror init-script template (dl.google.com/Maven Central are
blocked in Iran — same reason NuGet is mirrored)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Name + tagline («بازی حکم آنلاین») across i18n (app.title/subtitle),
layout metadata, PWA manifest, app icon, package name, server health
- Gameplay term «حکم» unchanged; repo/folder stay hokm/HokmPlay
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ui-store syncs screens to history (push on go/goGame, hash URLs like #/profile),
back() = history.back(), initHistory anchors a home base + restores deep links
- page.tsx listens to popstate; resolveScreen() guards transient screens
(game/room/chat/matchmaking fall back to home when their state is gone)
- ScreenHeader + ChatScreen back buttons use history back; chat cleans up on unmount
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>