auth: store-review test login + matchmaking no-hang/watchdog
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 56s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m7s

- 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 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-15 16:40:01 +03:30
parent a35acea7e4
commit 76c4b68a74
5 changed files with 29 additions and 2 deletions
+4
View File
@@ -67,6 +67,10 @@ services:
Sms__Provider: ${SMS_PROVIDER:-kavenegar} Sms__Provider: ${SMS_PROVIDER:-kavenegar}
Sms__ApiKey: ${SMS_API_KEY:-} Sms__ApiKey: ${SMS_API_KEY:-}
Sms__Template: ${SMS_TEMPLATE:-hokmotp} 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 panel (marketing-site links editor) — shared-token auth.
Admin__Token: ${ADMIN_TOKEN:-} Admin__Token: ${ADMIN_TOKEN:-}
# Where the admin-editable site-links JSON is persisted (mounted volume). # Where the admin-editable site-links JSON is persisted (mounted volume).
+15
View File
@@ -18,6 +18,14 @@ public sealed class SmsOptions
public string DevCode { get; set; } = "1234"; public string DevCode { get; set; } = "1234";
public int TtlSeconds { get; set; } = 120; public int TtlSeconds { get; set; } = 120;
/// <summary>
/// A reviewer/test login (Google Play, Bazaar, Myket): this exact phone never
/// triggers a real SMS and always accepts <see cref="TestCode"/>. Give these
/// to the store's review team. Set TestPhone empty to disable.
/// </summary>
public string TestPhone { get; set; } = "09120000000";
public string TestCode { get; set; } = "453115";
/* --- Rate limiting (applies to real SMS sends only; dev mode is unlimited) --- */ /* --- Rate limiting (applies to real SMS sends only; dev mode is unlimited) --- */
/// <summary>Minimum seconds between two OTP sends to the same phone (resend cooldown).</summary> /// <summary>Minimum seconds between two OTP sends to the same phone (resend cooldown).</summary>
public int ResendCooldownSeconds { get; set; } = 60; public int ResendCooldownSeconds { get; set; } = 60;
@@ -54,12 +62,18 @@ public sealed class OtpService
/// <summary>Dev mode = explicitly on, or no API key configured.</summary> /// <summary>Dev mode = explicitly on, or no API key configured.</summary>
public bool IsDev => _opts.DevMode || string.IsNullOrWhiteSpace(_opts.ApiKey); public bool IsDev => _opts.DevMode || string.IsNullOrWhiteSpace(_opts.ApiKey);
private bool IsTestPhone(string normalizedPhone) =>
!string.IsNullOrWhiteSpace(_opts.TestPhone) && normalizedPhone == Normalize(_opts.TestPhone);
/// <summary>Generate a code, store it, and send the SMS. Returns devCode only in dev mode.</summary> /// <summary>Generate a code, store it, and send the SMS. Returns devCode only in dev mode.</summary>
public async Task<OtpResult> Request(string phone) public async Task<OtpResult> Request(string phone)
{ {
phone = Normalize(phone); phone = Normalize(phone);
if (string.IsNullOrWhiteSpace(phone)) return new OtpResult(false, null, "INVALID_PHONE", 0); 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. // Dev mode never sends an SMS (fixed code) → no cost, no rate limit.
if (IsDev) if (IsDev)
{ {
@@ -131,6 +145,7 @@ public sealed class OtpService
public bool Verify(string phone, string code) public bool Verify(string phone, string code)
{ {
phone = Normalize(phone); phone = Normalize(phone);
if (IsTestPhone(phone)) return code == _opts.TestCode; // store-review test login
if (IsDev && code == _opts.DevCode) return true; if (IsDev && code == _opts.DevCode) return true;
if (!_codes.TryGetValue(phone, out var e)) return false; if (!_codes.TryGetValue(phone, out var e)) return false;
if (DateTime.UtcNow > e.Expires) { _codes.TryRemove(phone, out _); return false; } if (DateTime.UtcNow > e.Expires) { _codes.TryRemove(phone, out _); return false; }
@@ -122,7 +122,11 @@ export function MatchmakingScreen() {
{searching && ( {searching && (
<> <>
<div className="mt-2 text-3xl font-black gold-text tabular-nums">{elapsed}s</div> <div className="mt-2 text-3xl font-black gold-text tabular-nums">{elapsed}s</div>
{elapsed >= 28 ? (
<p className="text-rose-300 text-xs mt-2 max-w-[18rem]">{t("mm.stuck")}</p>
) : (
<p className="text-cream/50 text-xs mt-1 max-w-[16rem]">{t("mm.fillHint")}</p> <p className="text-cream/50 text-xs mt-1 max-w-[16rem]">{t("mm.fillHint")}</p>
)}
</> </>
)} )}
+3 -1
View File
@@ -48,8 +48,10 @@ export function OnlineLobbyScreen() {
go("buycoins"); go("buycoins");
return; 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"); go("matchmaking");
void startMatchmaking({ ranked: true, stake: l.entry });
}; };
return ( return (
+2
View File
@@ -256,6 +256,7 @@ const fa: Dict = {
"mm.found": "بازیکنان پیدا شدند!", "mm.found": "بازیکنان پیدا شدند!",
"mm.ready": "آماده شروع", "mm.ready": "آماده شروع",
"mm.fillHint": "اگر بازیکن آنلاینی پیدا نشود، ربات‌ها جایگزین می‌شوند", "mm.fillHint": "اگر بازیکن آنلاینی پیدا نشود، ربات‌ها جایگزین می‌شوند",
"mm.stuck": "اتصال به سرور طول کشید. لطفاً «لغو» را بزنید و دوباره تلاش کنید.",
"intro.found": "بازیکنان آماده‌اند!", "intro.found": "بازیکنان آماده‌اند!",
"intro.getReady": "بازی در حال شروع است…", "intro.getReady": "بازی در حال شروع است…",
"intro.go": "شروع!", "intro.go": "شروع!",
@@ -642,6 +643,7 @@ const en: Dict = {
"mm.found": "Players found!", "mm.found": "Players found!",
"mm.ready": "Ready to start", "mm.ready": "Ready to start",
"mm.fillHint": "If no online players are found, bots will fill in", "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.cancel": "Cancel",
"mm.start": "Enter game", "mm.start": "Enter game",