fix(offline): return online after flight-mode off; gate online-game actions offline
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m38s

Two mid-session issues found on the contour:
- Auto-offline did not return to online after the network came back: a single
  reachability check right after the 'online' event failed (the interface is up
  before the gateway is actually reachable again). Retry a few times with backoff
  (tryReturnOnline) — that is what actually gets the app back online.
- An online game viewed while offline (flight mode on) still enabled its network
  actions, so they hit the kill switch and raised 'something went wrong' toasts.
  Gate them: netReady = isLocalGame || (connection.online && !offlineMode.active)
  — a local game stays fully usable; an online game's make/exchange/hint/resign
  disable while offline and re-enable when back. Also suppress the 'offline' code
  in handleError (a blocked call in offline mode is expected, not a toast).
This commit is contained in:
Ilia Denisov
2026-07-06 18:35:52 +02:00
parent e0d28733ff
commit fc8143758a
2 changed files with 31 additions and 13 deletions
+19 -7
View File
@@ -304,7 +304,9 @@ export function handleError(err: unknown): void {
void enterBlocked();
return;
}
if (isConnectionCode(code) || !connection.online) return;
// A blocked call in offline mode ('offline', the transport kill switch) is expected, not an error
// to surface — the offline chrome already signals the state; never a red toast.
if (isConnectionCode(code) || code === 'offline' || !connection.online) return;
haptic('error');
showToast(t(code ? errorKey(code) : 'error.generic'), 'error');
}
@@ -585,18 +587,28 @@ function promptOfflineChoice(): Promise<boolean> {
* cold-start dialog) is left as the player's choice. These are passive OS events — no polling, no
* battery cost. Web-only and skipped in the mock (the e2e drives connectivity directly).
*/
// tryReturnOnline is fired from the online event while in auto-offline: the interface being back does
// not mean the gateway is reachable yet (DNS/routing settle for a moment after flight mode), so it
// re-checks a few times with backoff before returning online, stopping if the player takes over the
// state meanwhile. This bounded retry is what actually gets the app back online after flight mode off.
async function tryReturnOnline(): Promise<void> {
for (let attempt = 0; attempt < 4; attempt++) {
if (!offlineMode.active || !offlineMode.auto) return;
if (await checkReachable(BOOT_REACHABILITY_TIMEOUT_MS)) {
if (offlineMode.active && offlineMode.auto) setOfflineMode(false);
return;
}
await new Promise((r) => setTimeout(r, 600 * (attempt + 1)));
}
}
export function initNetworkReactivity(): void {
if (typeof window === 'undefined' || import.meta.env.MODE === 'mock') return;
window.addEventListener('offline', () => {
if (!offlineMode.active) setOfflineMode(true, false); // auto (session) — self-heals below
});
window.addEventListener('online', () => {
if (offlineMode.active && offlineMode.auto) {
void checkReachable(BOOT_REACHABILITY_TIMEOUT_MS).then((reachable) => {
// Re-read the flags — the player may have chosen offline meanwhile — before returning online.
if (reachable && offlineMode.active && offlineMode.auto) setOfflineMode(false);
});
}
if (offlineMode.active && offlineMode.auto) void tryReturnOnline();
});
}