Skip to content

Show the re-pair guide on Devices, not only on Live (#802) - #810

Merged
ryanbr merged 1 commit into
mainfrom
fix/repair-guide-on-devices
Jul 25, 2026
Merged

ryanbr merged 1 commit into
mainfrom
fix/repair-guide-on-devices

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 25, 2026

Copy link
Copy Markdown
Owner

A WHOOP 5/MG that connects but never finishes bonding leaves the user on Devices — that is where you go to fix a device. The four steps that resolve it rendered only on Live (LiveScreen.kt:262, LiveView.swift:92), one tab away.

#802 is the evidence. That reporter hit the #971 bond-watchdog path, bounced to the give-up threshold (reconnect n=4) so the guide was already armed, and filed an issue anyway — because Devices showed a disconnected strap and no reason for it.

Worth noting Devices already surfaces the adjacent case: bondRefused at DevicesScreen.kt:190 gates on live.pairingHint, which is the #78 explicit-refusal path (status 5/15). The watchdog timeout sets reconnectGuide instead — so of two neighbouring failure modes, only one said anything on this screen.

Rendering only. Both platforms already had live in scope there, so no state plumbing.

No new copy. Strings are reused verbatim from the Live banners — the iOS literal already resolves across de/es/fr/it/pt-PT/ru/zh-Hans/zh-Hant, and l10n_live_screen_can_t_connect_your_strap_s_cf78be83 already exists in the Android catalog. Nothing to translate.

The iOS banner is duplicated rather than hoisted: the two views share no banner container today, and the string — the part that must not drift — is shared.

Verified: helper sits inside DevicesContent (the struct owning @EnvironmentObject live), all Compose imports the new block needs were already present, braces/parens balanced on both files, i18n gate green. Not built — no Xcode or JDK here — though the next staging build covers it.

A WHOOP 5/MG that connects but never finishes bonding leaves the user on the Devices
screen, because that is where you go to fix a device. The four steps that actually
resolve it rendered only on Live (LiveScreen.kt:262 / LiveView.swift:92), one tab away.

#802 is the evidence: that reporter hit the #971 watchdog path, bounced to the give-up
threshold so the guide was armed, and filed an issue anyway — Devices showed them a
disconnected strap and no reason.

Devices did already surface the adjacent case: bondRefused at DevicesScreen.kt:190
gates on live.pairingHint, which is the #78 EXPLICIT-refusal path (status 5/15). The
watchdog timeout sets reconnectGuide instead, so of the two failure modes only one
said anything here.

Both platforms already had the live state in scope on this screen, so this is
rendering only — no state plumbing. Strings are reused verbatim from the Live banners,
so there is no new copy and nothing new to translate: the iOS literal already resolves
across eight locales and the Android key already exists.

The iOS banner is duplicated rather than hoisted because the two views share no banner
container today; the string is what must not drift, and that is shared.
@ryanbr
ryanbr force-pushed the fix/repair-guide-on-devices branch from a820fe4 to 3bfebaa Compare July 25, 2026 18:34
@ryanbr

ryanbr commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Re-reviewed the pushed diff. No defects. Recording what was checked:

Why the iOS banner is duplicated rather than extracted — the obvious review question. No shared warning-banner component exists on either platform: honestyBanner (InsightsHubView), errorBanner (CoachView), HonestyBanner (InsightsHubScreen) and three separate PrivacyNotes are all screen-local private functions. HealthAlertBanner is the one shared banner struct, but it reads model.healthAlert directly and is not general-purpose. So a screen-local copy is the convention here, not a shortcut. Extracting a shared banner would be a reasonable follow-up, but it would touch six existing call sites and is not this fix.

Placement — the Android block sits before the devices == null early return, so the guide shows even while the registry is still resolving. Deliberate: a user whose strap will not bond should not have to wait for the device list to populate before being told why.

Scope of state — live is declared at DevicesScreen.kt:113 and used at :169, same composable. On iOS the helper is inside DevicesContent, which owns @EnvironmentObject var live; the outer DevicesView explicitly does not observe LiveState (its own comment says so for perf), so this is the right struct.

Still unbuilt — no Xcode or JDK here. The next staging build covers both platforms.

@ryanbr

ryanbr commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Worth adding: this affects WHOOP 4.0 as well as 5/MG, and 4.0 was the worse-served case.

reconnectGuide is set by family-agnostic paths. armBondWatchdog() is called from kickServiceDiscovery, which runs for every connection with no connectedFamily gate, and its own comment calls it the "bonded-independent handshake watchdog (#50)". The #971 give-up comment describes a WHOOP 4.0 looping forever; the #802 reporter who hit the same path is on MG.

The coverage Devices already had is 5/MG-only: bondRefused at DevicesScreen.kt:190 gates on live.pairingHint, and noteBondRefusalForPairingHint returns early unless connectedFamily == WHOOP5.

4.0 5/MG
Explicit bond refusal (status 5/15) n/a — 4.0 bonds cleanly shown on Devices
Watchdog timeout (#971) nothing on Devices nothing on Devices

So a 4.0 stuck in a bond loop had no Devices signal at all, since the only existing indicator is gated to 5/MG. The MG report surfaced it, but 4.0 is the case with no fallback.

@ryanbr

ryanbr commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Third pass. One correction to my own earlier note, and one thing verified clean.

Correction: the "deliberate placement" claim only holds on Android. I said the Android block sits before the devices == null early return so the guide shows while the registry resolves, and framed that as a considered choice. It is — but iOS does not match it. DevicesContent is only rendered when model.deviceRegistry != nil; otherwise the outer DevicesView shows DataPendingNote and the banner never renders. So during that window Android shows the guide and iOS does not.

Not worth fixing. Hoisting the banner into the outer DevicesView would mean adding @EnvironmentObject var live there, and that view deliberately avoids observing LiveState — its comment says doing so would "re-render the whole screen on every ~1 Hz strap tick for no visible change". Trading a real perf regression for a sub-second difference during launch is a bad deal, and CLAUDE.md's parity rule is feature-level rather than pixel-level, which this comfortably clears.

So: each placement is right for its platform's structure, and the difference is now written down instead of being an unstated accident.

Verified clean: no stale-guide risk. reconnectGuide is cleared on connect unless postBondLoop.tripped, and even then only held until the same continuous connection survives the quick-timeout window (connectGeneration guards against a transient cycle satisfying it). A user whose strap reconnects successfully will not be left reading re-pair steps on Devices. That mattered more here than on Live, since Devices is the screen people sit on while fixing a strap.

@ryanbr
ryanbr merged commit 956727e into main Jul 25, 2026
1 check passed
ryanbr added a commit that referenced this pull request Jul 25, 2026
iOS 208 -> 209, Android 299 -> 300. MARKETING_VERSION/versionName stay 9.1.1:
nothing since the last staging build warrants a version bump — an in-repo FAQ,
a re-pair-guide placement fix, and a diagnostic log field.

Picks up three commits since a0970b1:
  0105c64 docs: in-repo FAQ, issue templates repointed off the dead wiki (#809)
  956727e re-pair guide on Devices, not only on Live (#802) (#810)
  3c0fa7c rrIntegrity verdict on the hrv diag line (#550) (#811)
@ryanbr
ryanbr deleted the fix/repair-guide-on-devices branch August 2, 2026 07:41
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…nbr#810)

A strap that connects but never finishes bonding leaves the user on the Devices
screen, because that is where you go to fix a device. The four steps that resolve it
rendered only on Live (LiveScreen.kt:262 / LiveView.swift:92), one tab away. The ryanbr#802
reporter bounced to the watchdog give-up threshold, so the guide was already armed,
and filed an issue anyway because Devices showed a disconnected strap and no reason.

Affects both strap families, and 4.0 was the worse-served case. reconnectGuide is set
by family-agnostic paths: armBondWatchdog runs from kickServiceDiscovery with no
family gate. The coverage Devices already had is 5/MG-only, since bondRefused gates on
live.pairingHint and noteBondRefusalForPairingHint returns early unless WHOOP5. So a
4.0 in a bond loop had no Devices signal at all.

Rendering only: both platforms already had the live state in scope on this screen.
Strings are reused verbatim from the Live banners, so nothing new to translate — the
iOS literal already resolves across eight locales and the Android key already exists.

The iOS banner is duplicated rather than hoisted because no shared warning-banner
component exists on either platform; every banner is screen-local. HealthAlertBanner
is the one shared struct and it reads model.healthAlert directly.

One deliberate asymmetry: on Android the block sits before the devices == null early
return so the guide shows while the registry resolves; on iOS DevicesContent is not
rendered in that window. Hoisting it would require the outer DevicesView to observe
LiveState, which it avoids on purpose to keep a ~1 Hz strap tick from re-rendering the
whole screen. Feature-level parity holds.

This is discoverability only. It does not fix the bond failure — the cause is a stale
pairing or the official WHOOP app holding the strap, which no client-side change can
resolve.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
iOS 208 -> 209, Android 299 -> 300. MARKETING_VERSION/versionName stay 9.1.1:
nothing since the last staging build warrants a version bump — an in-repo FAQ,
a re-pair-guide placement fix, and a diagnostic log field.

Picks up three commits since a0970b1:
  0105c64 docs: in-repo FAQ, issue templates repointed off the dead wiki (ryanbr#809)
  956727e re-pair guide on Devices, not only on Live (ryanbr#802) (ryanbr#810)
  3c0fa7c rrIntegrity verdict on the hrv diag line (ryanbr#550) (ryanbr#811)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant