auth: store-review test login + matchmaking no-hang/watchdog
- 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:
@@ -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).
|
||||
|
||||
@@ -18,6 +18,14 @@ public sealed class SmsOptions
|
||||
public string DevCode { get; set; } = "1234";
|
||||
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) --- */
|
||||
/// <summary>Minimum seconds between two OTP sends to the same phone (resend cooldown).</summary>
|
||||
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>
|
||||
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>
|
||||
public async Task<OtpResult> 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; }
|
||||
|
||||
@@ -122,7 +122,11 @@ export function MatchmakingScreen() {
|
||||
{searching && (
|
||||
<>
|
||||
<div className="mt-2 text-3xl font-black gold-text tabular-nums">{elapsed}s</div>
|
||||
<p className="text-cream/50 text-xs mt-1 max-w-[16rem]">{t("mm.fillHint")}</p>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user