From 76c4b68a7480846b67207ab623864a21598f21c4 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 15 Jun 2026 16:40:01 +0330 Subject: [PATCH] auth: store-review test login + matchmaking no-hang/watchdog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OtpService: a designated test phone (default 09120000000 / code 453115, overridable via Sms__TestPhone/Sms__TestCode) skips real SMS and always verifies — for Google Play / Bazaar / Myket reviewers. Give them these creds. - Matchmaking UX: tapping a league now navigates to the matchmaking screen BEFORE awaiting the SignalR handshake, so the button can't freeze. Added a watchdog hint after 28s ("connection took too long, cancel & retry") so it never spins forever when the hub doesn't connect. Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 4 ++++ server/src/Hokm.Server/Auth/OtpService.cs | 15 +++++++++++++++ src/components/screens/MatchmakingScreen.tsx | 6 +++++- src/components/screens/OnlineLobbyScreen.tsx | 4 +++- src/lib/i18n.tsx | 2 ++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7ec3541..1844433 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,6 +67,10 @@ services: Sms__Provider: ${SMS_PROVIDER:-kavenegar} Sms__ApiKey: ${SMS_API_KEY:-} Sms__Template: ${SMS_TEMPLATE:-hokmotp} + # Store-review test login (Google Play / Bazaar / Myket): this phone skips + # SMS and always accepts the static code. Give these to the review team. + Sms__TestPhone: ${SMS_TEST_PHONE:-09120000000} + Sms__TestCode: ${SMS_TEST_CODE:-453115} # Admin panel (marketing-site links editor) — shared-token auth. Admin__Token: ${ADMIN_TOKEN:-} # Where the admin-editable site-links JSON is persisted (mounted volume). diff --git a/server/src/Hokm.Server/Auth/OtpService.cs b/server/src/Hokm.Server/Auth/OtpService.cs index 4db5716..4d72ec8 100644 --- a/server/src/Hokm.Server/Auth/OtpService.cs +++ b/server/src/Hokm.Server/Auth/OtpService.cs @@ -18,6 +18,14 @@ public sealed class SmsOptions public string DevCode { get; set; } = "1234"; public int TtlSeconds { get; set; } = 120; + /// + /// A reviewer/test login (Google Play, Bazaar, Myket): this exact phone never + /// triggers a real SMS and always accepts . Give these + /// to the store's review team. Set TestPhone empty to disable. + /// + public string TestPhone { get; set; } = "09120000000"; + public string TestCode { get; set; } = "453115"; + /* --- Rate limiting (applies to real SMS sends only; dev mode is unlimited) --- */ /// Minimum seconds between two OTP sends to the same phone (resend cooldown). public int ResendCooldownSeconds { get; set; } = 60; @@ -54,12 +62,18 @@ public sealed class OtpService /// Dev mode = explicitly on, or no API key configured. public bool IsDev => _opts.DevMode || string.IsNullOrWhiteSpace(_opts.ApiKey); + private bool IsTestPhone(string normalizedPhone) => + !string.IsNullOrWhiteSpace(_opts.TestPhone) && normalizedPhone == Normalize(_opts.TestPhone); + /// Generate a code, store it, and send the SMS. Returns devCode only in dev mode. public async Task Request(string phone) { phone = Normalize(phone); if (string.IsNullOrWhiteSpace(phone)) return new OtpResult(false, null, "INVALID_PHONE", 0); + // Store review/test login: never send an SMS for the designated test number. + if (IsTestPhone(phone)) return new OtpResult(true, null, null, 0); + // Dev mode never sends an SMS (fixed code) → no cost, no rate limit. if (IsDev) { @@ -131,6 +145,7 @@ public sealed class OtpService public bool Verify(string phone, string code) { phone = Normalize(phone); + if (IsTestPhone(phone)) return code == _opts.TestCode; // store-review test login if (IsDev && code == _opts.DevCode) return true; if (!_codes.TryGetValue(phone, out var e)) return false; if (DateTime.UtcNow > e.Expires) { _codes.TryRemove(phone, out _); return false; } diff --git a/src/components/screens/MatchmakingScreen.tsx b/src/components/screens/MatchmakingScreen.tsx index d0c17a4..9c8074a 100644 --- a/src/components/screens/MatchmakingScreen.tsx +++ b/src/components/screens/MatchmakingScreen.tsx @@ -122,7 +122,11 @@ export function MatchmakingScreen() { {searching && ( <>
{elapsed}s
-

{t("mm.fillHint")}

+ {elapsed >= 28 ? ( +

{t("mm.stuck")}

+ ) : ( +

{t("mm.fillHint")}

+ )} )} diff --git a/src/components/screens/OnlineLobbyScreen.tsx b/src/components/screens/OnlineLobbyScreen.tsx index 85032ea..3cd04b8 100644 --- a/src/components/screens/OnlineLobbyScreen.tsx +++ b/src/components/screens/OnlineLobbyScreen.tsx @@ -48,8 +48,10 @@ export function OnlineLobbyScreen() { go("buycoins"); return; } - await startMatchmaking({ ranked: true, stake: l.entry }); + // Navigate FIRST so the button never hangs on the SignalR handshake; the + // matchmaking screen shows "searching" while the connection establishes. go("matchmaking"); + void startMatchmaking({ ranked: true, stake: l.entry }); }; return ( diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx index ab00b61..1ac9bdb 100644 --- a/src/lib/i18n.tsx +++ b/src/lib/i18n.tsx @@ -256,6 +256,7 @@ const fa: Dict = { "mm.found": "بازیکنان پیدا شدند!", "mm.ready": "آماده شروع", "mm.fillHint": "اگر بازیکن آنلاینی پیدا نشود، ربات‌ها جایگزین می‌شوند", + "mm.stuck": "اتصال به سرور طول کشید. لطفاً «لغو» را بزنید و دوباره تلاش کنید.", "intro.found": "بازیکنان آماده‌اند!", "intro.getReady": "بازی در حال شروع است…", "intro.go": "شروع!", @@ -642,6 +643,7 @@ const en: Dict = { "mm.found": "Players found!", "mm.ready": "Ready to start", "mm.fillHint": "If no online players are found, bots will fill in", + "mm.stuck": "Connecting to the server is taking too long. Tap Cancel and try again.", "mm.cancel": "Cancel", "mm.start": "Enter game",