You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Oura: a ring with no advertised name lists with a blank label on both platforms #1783
Surfaced while reviewing #1776, which is merged and correct — this is the gap it makes reachable, not a defect in it.
Now that a bonded Oura ring advertising no local name is listed, name is "" all the way to the UI, and neither platform guards it.
Android
AddDeviceWizard.kt:1339 passes the raw name into the picker row:
DiscoveredRow(
name = ring.name,
subtitle = ring.detectedGen?.displayName ?:"Oura ring",
and DiscoveredRow renders it unguarded at :1641:
Text(name, style =NoopType.body, color =Palette.textPrimary)
So the row draws an empty primary line above the "Oura ring" subtitle. It is still tappable and the subtitle identifies it, which is why this is not a blocker — but an empty title reads as a broken row rather than as "this ring has no name".
Apple
AddDeviceWizard.swift:1160 has the same gap in the confirm step's name draft:
ifletpickedOura{returnpickedOura.ring.name}
This one is pre-existing — Apple has always listed nameless rings — so an empty name draft has been reachable there for as long as the feature has existed.
So the pattern to copy is in the same function. The Oura equivalent wants the same shape — fall back to a generic ring label when the advertised name is blank — on both platforms, and on Android in DiscoveredRow's caller rather than in the row itself, so the row stays a dumb renderer.
Scope
New user-visible copy on both platforms, so it needs the Android l10n_ key and all eight iOS locales in the xcstrings catalogue.
A detectedGen of null already falls back to GEN3 in the wizard, so no further wiring is needed to actually add the ring — this is purely how it is labelled.
Worth a JVM test on the Android side pinning that a blank advertised name produces a non-blank row title, since that is assertable without a radio even though discovery itself is not.
Thanks @pipiche38 — the nameless-ring case only became visible because the discovery bug was fixed first.
Apple confirmAdvertisedName — copy the WHOOP empty-name fallback already four lines up:
if let pickedOura {
return pickedOura.ring.name.isEmpty
? (type.map(typeTitle) ?? String(localized: "Device"))
: pickedOura.ring.name
}
Android DiscoveredRow caller — fall back before the row so the row stays a dumb renderer:
name = ring.name.ifBlank {
uiString(R.string.l10n_…_oura_ring_fallback) // new key; keep subtitle as detectedGen / "Oura ring"
}
Scope: new user-visible copy on both platforms (Android l10n_ key + all eight iOS xcstrings locales). A detectedGen of null already falls back to GEN3 in the wizard, so no further adopt wiring. JVM test on the Android name helper if a cheap pure function is extractable; otherwise the Apple change is the pattern source of truth.
@kvnloo — flagging before you spend time on it, since your plan follows the issue as written: the
guard you'd be adding is already there one layer down. The parity and i18n bits above are what's
actually left, and they're small.
Checked this before writing the guard, and the premise doesn't hold — on either platform.
DiscoveredRing.name is never blank. Both sides already substitute a fallback at the single
construction site, and have since the original Oura commit (4897f26e), not since #1776:
// android/…/ble/OuraLiveSource.kt
name = name.ifBlank { "Oura" },
Those are the only two places a DiscoveredRing is constructed on either platform, so every call
site listed in the issue — AddDeviceWizard.kt:1339, AddDeviceWizard.swift:1160, and AddDeviceWizard.swift:1646, which has the same shape and isn't listed — receives an
already-guarded value. A nameless bonded ring lists as "Oura" over its subtitle, not as an
empty line. Adding the .isEmpty fallback at those call sites would be dead code, and the JVM test
pinning it would pin nothing.
There is something real in that code, though, just not the reported symptom.
The two guards disagree.ifBlank treats an all-whitespace name as absent; isEmpty does not.
A ring advertising " " lists as "Oura" on Android and blank on Apple. That's the silent
Swift/Kotlin drift the parity contract warns about — both lines read correctly on their own, and
only the pair is wrong.
I've folded both into one named helper, ouraAdvertisedLabel, whitespace-aware on both sides and
returning the name untrimmed when present, pinned by an oracle test whose expected values are the
Swift twin's own stdout rather than a reading of the Kotlin. That caught a bug in my own first
draft within minutes: I'd written advertisedName.trim().ifEmpty { fallback }, which returns the trimmed name where Swift returns the original. Side-by-side review would not have caught it; the " Oura Ring 4 " case does.
One genuine i18n bug in the same expression. The Android subtitle fallback was a hardcoded
English literal:
while l10n_add_device_wizard_oura_ring_e3431536 already exists in all six locales. That fallback
renders exactly when the generation is undetected — which is the nameless bonded-ring case from #1776 — so a non-English user hit untranslated copy on the one row this issue is about. Fixed with
the existing key, so no new string on either platform.
Verified: Android 5040 unit tests (4/4 in OuraNamelessRingDiscoveryTest, including the new oracle
case); macOS xcodebuild -scheme Strand succeeds, since app-target Swift isn't covered by the
default CI jobs; doc_comment_lint and i18n_audit --ci both clean. No hardware run — this
changes a label, not the BLE path, and discovery behaviour is untouched.
Closing. The reported symptom was never real — my error, and @pipiche38 caught it before anyone
built the wrong thing.
The premise was wrong
This issue asserted that name is "" all the way to the UI and that "neither platform guards
it". Both guards were already there, one layer down at the single DiscoveredRing construction
site, and had been since the original Oura commit:
4897f26ef 2026-06-29 Add local Oura ring support (Gen 3/4/5), on-device, beta
7b566f578 2026-09-01 fix(oura): list a bonded ring that advertises no local name (#1776)
git log -S on both fallback expressions returns 4897f26ef — two months before #1776, so this
was not a gap #1776 made reachable. A nameless bonded ring has always listed as "Oura" over its
subtitle, never as an empty line. The guard at AddDeviceWizard.kt:1339 and AddDeviceWizard.swift:1160 that this issue asked for would have been dead code, and the JVM test
it asked for would have pinned nothing.
How I got there is worth recording: I read the two render sites, saw no guard, and reported the
absence of a guard anywhere as a fact — without checking the construction site one layer down.
Same shape as the sweep failure in #1787: looking where the bug was expected and treating a
negative result there as a global negative.
What was actually real, and where it landed
The investigation was not wasted — it surfaced two genuine defects, neither of them the reported
one, both now on main via #1797:
A silent Swift/Kotlin divergence. The two guards disagreed: ifBlank treats an
all-whitespace name as absent, isEmpty does not, so a ring advertising " " listed as "Oura"
on Android and blank on Apple. Both lines read correctly alone; only the pair was wrong. Now one
helper, ouraAdvertisedLabel, on both platforms, pinned by an oracle test whose expectations are
the Swift twin's own stdout — which caught a trim()-based draft that returned the trimmed name
where Swift returns the original. " Oura Ring 4 " is the separating case.
A real i18n bug in the same expression. The Android subtitle fallback was a hardcoded
English "Oura ring" while l10n_add_device_wizard_oura_ring_e3431536 already existed in all
seven locale files — and it renders exactly when the generation is undetected, which is the
nameless bonded-ring case. Now uses the existing key, matching the localisation Apple already
had. No new string on either platform.
One follow-up of mine on top (206a6fff9): the helper's doc claimed the two stdlibs part on
U+00A0. They do not — ifBlank tests Kotlin's Char.isWhitespace, which is Character.isWhitespace(c) || Character.isSpaceChar(c), and the second disjunct covers exactly the
non-breaking spaces the first drops. The real divergence is U+0085 and U+001C–U+001F.
Thanks @pipiche38 for checking the premise instead of implementing it, and @kvnloo for picking it
up — the flag came before you'd spent the time, which is the system working.
Activity
staging: build 406 / iOS 287 for the standard-HR contact work
kvnloo commentedon Sep 1, 2026
Happy to take this. Plan matches the issue:
Apple
confirmAdvertisedName— copy the WHOOP empty-name fallback already four lines up:Android
DiscoveredRowcaller — fall back before the row so the row stays a dumb renderer:Scope: new user-visible copy on both platforms (Android
l10n_key + all eight iOS xcstrings locales). AdetectedGenof null already falls back to GEN3 in the wizard, so no further adopt wiring. JVM test on the Android name helper if a cheap pure function is extractable; otherwise the Apple change is the pattern source of truth.Abort if someone else is already on it.
pipiche38 commentedon Sep 1, 2026
@kvnloo — flagging before you spend time on it, since your plan follows the issue as written: the
guard you'd be adding is already there one layer down. The parity and i18n bits above are what's
actually left, and they're small.
Checked this before writing the guard, and the premise doesn't hold — on either platform.
DiscoveredRing.nameis never blank. Both sides already substitute a fallback at the singleconstruction site, and have since the original Oura commit (
4897f26e), not since #1776:Those are the only two places a
DiscoveredRingis constructed on either platform, so every callsite listed in the issue —
AddDeviceWizard.kt:1339,AddDeviceWizard.swift:1160, andAddDeviceWizard.swift:1646, which has the same shape and isn't listed — receives analready-guarded value. A nameless bonded ring lists as "Oura" over its subtitle, not as an
empty line. Adding the
.isEmptyfallback at those call sites would be dead code, and the JVM testpinning it would pin nothing.
There is something real in that code, though, just not the reported symptom.
The two guards disagree.
ifBlanktreats an all-whitespace name as absent;isEmptydoes not.A ring advertising
" "lists as "Oura" on Android and blank on Apple. That's the silentSwift/Kotlin drift the parity contract warns about — both lines read correctly on their own, and
only the pair is wrong.
I've folded both into one named helper,
ouraAdvertisedLabel, whitespace-aware on both sides andreturning the name untrimmed when present, pinned by an oracle test whose expected values are the
Swift twin's own stdout rather than a reading of the Kotlin. That caught a bug in my own first
draft within minutes: I'd written
advertisedName.trim().ifEmpty { fallback }, which returns thetrimmed name where Swift returns the original. Side-by-side review would not have caught it; the
" Oura Ring 4 "case does.One genuine i18n bug in the same expression. The Android subtitle fallback was a hardcoded
English literal:
while
l10n_add_device_wizard_oura_ring_e3431536already exists in all six locales. That fallbackrenders exactly when the generation is undetected — which is the nameless bonded-ring case from
#1776 — so a non-English user hit untranslated copy on the one row this issue is about. Fixed with
the existing key, so no new string on either platform.
Verified: Android 5040 unit tests (4/4 in
OuraNamelessRingDiscoveryTest, including the new oraclecase); macOS
xcodebuild -scheme Strandsucceeds, since app-target Swift isn't covered by thedefault CI jobs;
doc_comment_lintandi18n_audit --ciboth clean. No hardware run — thischanges a label, not the BLE path, and discovery behaviour is untouched.
Proposing: #1797
fix(oura): make the nameless-ring label rule one tested twin, not two (…
ryanbr commentedon Sep 2, 2026
Closing. The reported symptom was never real — my error, and @pipiche38 caught it before anyone
built the wrong thing.
The premise was wrong
This issue asserted that
nameis""all the way to the UI and that "neither platform guardsit". Both guards were already there, one layer down at the single
DiscoveredRingconstructionsite, and had been since the original Oura commit:
git log -Son both fallback expressions returns4897f26ef— two months before #1776, so thiswas not a gap #1776 made reachable. A nameless bonded ring has always listed as "Oura" over its
subtitle, never as an empty line. The guard at
AddDeviceWizard.kt:1339andAddDeviceWizard.swift:1160that this issue asked for would have been dead code, and the JVM testit asked for would have pinned nothing.
How I got there is worth recording: I read the two render sites, saw no guard, and reported the
absence of a guard anywhere as a fact — without checking the construction site one layer down.
Same shape as the sweep failure in #1787: looking where the bug was expected and treating a
negative result there as a global negative.
What was actually real, and where it landed
The investigation was not wasted — it surfaced two genuine defects, neither of them the reported
one, both now on main via #1797:
ifBlanktreats anall-whitespace name as absent,
isEmptydoes not, so a ring advertising" "listed as "Oura"on Android and blank on Apple. Both lines read correctly alone; only the pair was wrong. Now one
helper,
ouraAdvertisedLabel, on both platforms, pinned by an oracle test whose expectations arethe Swift twin's own stdout — which caught a
trim()-based draft that returned the trimmed namewhere Swift returns the original.
" Oura Ring 4 "is the separating case.English
"Oura ring"whilel10n_add_device_wizard_oura_ring_e3431536already existed in allseven locale files — and it renders exactly when the generation is undetected, which is the
nameless bonded-ring case. Now uses the existing key, matching the localisation Apple already
had. No new string on either platform.
One follow-up of mine on top (
206a6fff9): the helper's doc claimed the two stdlibs part onU+00A0. They do not —
ifBlanktests Kotlin'sChar.isWhitespace, which isCharacter.isWhitespace(c) || Character.isSpaceChar(c), and the second disjunct covers exactly thenon-breaking spaces the first drops. The real divergence is U+0085 and U+001C–U+001F.
Thanks @pipiche38 for checking the premise instead of implementing it, and @kvnloo for picking it
up — the flag came before you'd spent the time, which is the system working.