fix(connectivity): stop spurious 'Wallet is offline' banners on intermittent connections - #669
Merged
Merged
Conversation
…mittent connections - Probe our own API for reachability instead of clients3.google.com, which produces false offline states on networks where it is slow or filtered (NetInfo.configure in the Apollo client provider) - Initialize Firebase App Check once and use the cached token instead of re-initializing and force-refreshing on every GraphQL request — this added an attestation round-trip per call and was a frequent source of 'Network request failed' - Add useConnectivity hook with 3s offline hysteresis so momentary blips never surface, and a justReconnected signal - Home screen silently refetches when connectivity returns - Suppress pure network errors in the home Info banner (retries usually succeed; real offline state is handled by the debounced banner) - Clearer offline copy: 'No connection — showing cached balances' First step toward full offline capability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o HEAD Also guard the App Check init-retry against clobbering a newer attempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate machines Review follow-ups: - reachabilityTest now requires exactly the wallet API's 400-to-bare-GET signature. status<500 let captive portals (200/30x) read as reachable, which combined with the suppressed network errors meant stale balances with no offline indicator — the failure the google-204 probe caught. - "No connection — showing cached balances" moved to i18n (HomeScreen.offlineShowingCached, all 23 locales). - useConnectivity: 7 tests via consumer-effect harness with fake timers (blip never latches, 3s latches, instant recovery, exactly-one pulse per offline period, flapping restarts debounce, null = online). - getAppCheckToken: 3 tests (init-once memo, init-failure retry, transient token failure keeps the instance). - Stale "getDeviceToken error" log label corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
islandbitcoin
force-pushed
the
fix/intermittent-offline-banner
branch
from
July 17, 2026 07:05
524777a to
25a7406
Compare
…, show server errors Adversarial-review round 1 (4 confirmed findings): - NetInfo.configure() moved behind a once-per-session guard. It tears down the library's shared state and orphans every mounted subscriber (useNetInfo never re-subscribes), and it sat in the client effect that re-runs on every auth-token change — one account upgrade mid-session and the banner + reconnect refetch silently died. Runs once, before any subscriber can mount. - justReconnected now pulses only on a CONFIRMED recovery (isInternetReachable === true). With useNativeReachability:false, NetInfo emits false→null→true; pulsing at null fired the refetch into a dead network and consumed the latch so the true edge never pulsed. - Info.tsx suppression narrowed to transport-level failures: ServerError / ServerParseError (non-2xx, carry statusCode) keep displaying — suppressing them hid backend incidents the probe can't see. - Reconnect refetches catch rejections (fire on shaky edges). - 3 new tests: blip-after-real-offline (kills the wasOfflineRef-reset mutant found by mutation testing), false→null→true pulses once at the confirmed edge, false→null→false keeps the latch and re-latches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…den tests Adversarial-review round 2 (3 confirmed findings): - OFFLINE_DEBOUNCE_MS 3s → 8s. NetInfo re-probes no sooner than reachabilityShortTimeout (5s) after a failure, so a 3s debounce latched the banner on EVERY isolated probe failure (one 502 during a deploy, a killed socket on foreground) — arithmetically guaranteed, then a synchronized refetch burst on recovery. 8s absorbs one full failed-probe cycle; only consecutive failures surface. The two constants are cross-referenced in both files, the spec derives all timings from the exported constant, and a dedicated test asserts the coupling (fails loudly if they decouple). - App Check spec: concurrency test holds init open across overlapping calls (kills the memoize-after-await mutant that would fire one attestation per parallel cold-start operation) and asserts isTokenAutoRefreshEnabled:true (the option getToken(false) depends on). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
islandbitcoin
pushed a commit
that referenced
this pull request
Jul 18, 2026
main's schema snapshot (synced from the flash#452 branch) predated the Plaid fields, so neither side was a superset: public-schema.graphql now carries the union, with the three Plaid members verified field-by-field against the live production API via introspection. Codegen and i18n-types regenerated from the merged sources; hooks barrel keeps all three new exports. Full suite on the union: 42 suites / 194 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Users intermittently see "Wallet is offline" on the home screen when no deploy is happening and the backend is healthy. Two client-side causes:
clients3.google.com/generate_204. On networks where that endpoint is slow or filtered,isInternetReachableflips tofalseand the banner shows — even though the Flash API is perfectly reachable.appCheckLinkraninitializeAppCheck()+getToken(true)(force refresh) per call, adding a full attestation round-trip to every query — slow, and a frequent source of "Network request failed".Changes
NetInfo.configuresends a GET togaloyInstance.graphqlUri; only the wallet API's own 400-to-bare-GET signature counts as reachable, so a captive portal's 200/30x reads as unreachable (the protection the google-204 probe provided, kept). Configured exactly once per JS session, before any subscriber mounts —NetInfo.configure()tears down the library's shared state and orphans every mounted listener, so it must never re-run (it previously sat in the client effect that re-runs on token change).getToken(false)).useConnectivityhook with an 8s offline debounce — deliberately longer than one full failed-probe cycle (reachabilityShortTimeout5s + probe RTT; the constants are cross-referenced in both files and the coupling is test-enforced), so a single failed probe (one 502 during a deploy, a killed socket on foreground) never flashes the banner.justReconnectedpulses only on the confirmed (true) edge of NetInfo's realfalse→null→truerecovery sequence.ServerError/ServerParseError(non-2xx, carrystatusCode) keep displaying, so a backend incident the probe can't see is never hidden.HomeScreen.offlineShowingCached("No connection — showing cached balances"), en + all 23 locales.Testing
__tests__/hooks/use-connectivity.spec.tsx— 10 tests, all timings derived from the exportedOFFLINE_DEBOUNCE_MS: single-failed-probe-cycle absorption (asserts the constant coupling), debounce boundary, confirmed-edge pulse (false→null→true), failed-probe re-latch (false→null→false), blip-after-real-offline (kills the stale-latch mutant found by mutation testing), flapping restart, exactly-one-pulse-per-offline-period.__tests__/hooks/use-device-token.spec.ts— 4 tests: init-once withisTokenAutoRefreshEnabled: trueasserted, overlapping-calls-during-init (kills the memoize-after-await mutant), init-failure retry, transient token failure keeps the singleton.yarn tsc:checkclean; changed files lint clean; translation-drift clean.Notes
First step toward full offline capability. Follow-ups:
getErrorMessagesstill carries a now-unreachable "Wallet is offline" mapping (pre-existing); cache persistor trimming and an offline mutation queue are the next milestones. A dev-screen instance switch retargets the probe only after an app reload (documented inclient.tsx).🤖 Generated with Claude Code