Skip to content
Open
17 changes: 17 additions & 0 deletions ios/Runner/AccessorySetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ enum AccessorySetup {
message: "AccessorySetupKit requires iOS 18", details: nil))
}

case "removeBand":
if #available(iOS 18.0, *), let id = call.arguments as? String {
Impl.shared.removeBand(id: id) { result(nil) }
} else {
result(nil)
}

case "removeAll":
if #available(iOS 18.0, *) {
Impl.shared.removeAll { result(nil) }
Expand Down Expand Up @@ -468,6 +475,16 @@ private final class Impl {
remove({ a in !(self.service(of: a).map(sensors.contains) ?? false) }, completion)
}

/// Retire exactly the former main band; keep the replacement and all sensors.
func removeBand(id: String, _ completion: @escaping () -> Void) {
let sensors = sensorServices
let wanted = id.uppercased()
remove({ a in
a.bluetoothIdentifier?.uuidString.uppercased() == wanted
&& !(self.service(of: a).map(sensors.contains) ?? false)
}, completion)
}

/// Deprovisions the SENSOR with CoreBluetooth id [id] (called when it is forgotten), so
/// the next `showSensorPicker` opens a sheet instead of handing back the forgotten one.
/// Never removes a band.
Expand Down
9 changes: 9 additions & 0 deletions lib/ble/accessory_setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ class AccessorySetup {
} catch (_) {}
}

/// Retire only the old band after the replacement is durable. Best-effort;
/// a failed native removal leaves its approval, never clears the new one.
static Future<void> removeBand(String id) async {
if (!Platform.isIOS) return;
try {
await _ch.invokeMethod('removeBand', id.toUpperCase());
} catch (_) {}
}

/// Deprovision every ASK band, sensors kept (called on unpair). Best-effort.
static Future<void> removeAll() async {
if (!Platform.isIOS) return;
Expand Down
23 changes: 15 additions & 8 deletions lib/data/db.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4323,17 +4323,24 @@ class LocalDb {
// phone is the primary device's own pedometer, so it keeps `''` too —
// `source` is what tells wrist from pocket.
String deviceId = kPrimaryDeviceId,
// The generation when the caller's session began, before async reads.
int? resetGeneration,
}) async {
final generation = resetGeneration ?? ResetGate.generation;
final w = sanitizeCoverageWindow(startTs, endTs, steps);
if (w == null) return;
final db = await instance;
await db.insert('live_coverage', {
'start_ts': w.startTs,
'end_ts': w.endTs,
'steps': steps,
'day': day,
'source': source,
'device_id': deviceId,
// The wipe uses this gate too: a check before queuing can still race it.
await _withCommitGate(() async {
_refuseAfterReset(generation);
final db = await instance;
await db.insert('live_coverage', {
'start_ts': w.startTs,
'end_ts': w.endTs,
'steps': steps,
'day': day,
'source': source,
'device_id': deviceId,
});
});
}

Expand Down
31 changes: 30 additions & 1 deletion lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3613,6 +3613,33 @@
"pairModeTitle": "In den Kopplungsmodus versetzen",
"pairModeNoSite": "Ein generisches Gerät ohne einzelnen Hersteller, daher gibt es keine Herstelleranleitung.",
"pairOuraExistingKeyGuidance": "Nutzt du schon die Oura-App? Lass den Ring in der App gekoppelt und kopple ihn mit seinem vorhandenen Schlüssel. Überspringe die Schritte zum Zurücksetzen unten. Setze ihn nur zurück, wenn du einen neuen Schlüssel möchtest.",
"pairAddDevice": "Gerät hinzufügen",
"pairKindAll": "Alle",
"pairKindStrap": "Brustgurte & Armsensoren",
"pairKindRing": "Ringe",
"pairKindWatch": "Uhren",
"pairGoBack": "Zurück",
"pairAddDeviceSub": "Ringe, Uhren und Herzfrequenzgurte",
"pairAddDeviceSubtitle": "Bei jedem Gerät steht, wofür es passt: jeden Tag oder nur beim Training.",
"pairBandFoundBody": "Jeden Tag kann es viel mehr als nur beim Training. Was möchtest du?",
"pairBandFoundBodyHasBand": "Du hast schon ein Band. Es kann immer nur eines dein Band sein.",
"pairIdentifiedBody": "Es sah wie ein Herzfrequenzgurt aus, hat sich beim Verbinden aber als {label} gemeldet. Es wurde nichts gespeichert.",
"pairAsMyBand": "Als mein Band koppeln",
"pairSecondaryPaired": "Fürs Training gekoppelt.",
"pairBandNow": "Dein Band jetzt: {name}. Deine Daten bleiben beim Wechsel erhalten.",
"pairReplaceBand": "Dieses Band koppeln, dann {name} trennen",
"pairReplaceConfirmTitle": "{name} nach erfolgreicher Verbindung ersetzen?",
"pairReplaceConfirmBody": "Dein bisheriges Band bleibt bei Fehlern oder Abbruch gekoppelt. Deine Daten bleiben erhalten.",
"pairReplaceConfirmYes": "Ersatz koppeln",
"pairCurrentBandFallback": "dein Band",
"pairWearableLead": "Es kann deine Tage mit Schlaf, Erholung und Herzfrequenz füllen.",
"pairWearableNote": "Füllt Tage ohne dein WHOOP. Experimentell, also schalte es unter Geräte ein.",
"pairStrapLead": "Es liefert Herzfrequenz beim Training. Schlaf, Erholung und tägliche Belastung zeichnet es nicht auf.",
"pairStrapNote": "Für beides muss „Beim Training nutzen“ an sein.",
"pairDeviceNamed": "{name} koppeln",
"pairUseForWorkouts": "Nur fürs Training nutzen",
"pairReplaceUnpairFailed": "Dein Band ließ sich nicht trennen, daher wurde das neue nicht gekoppelt. Versuche es erneut.",
"pairWearableLeadPebble": "Pebble 2 kann deine Tage mit Schlaf, Erholung und Herzfrequenz füllen. Pebble 2 SE zeigt die eigenen Schlaf- und Schrittdaten der Uhr. Sie hat keinen Herzfrequenzsensor, daher kann diese App daraus weder die tägliche Belastung berechnen noch die Erholung schätzen.",
"welcomeCoachKeyReentry": "Dein API-Schlüssel für den Coach bleibt im sicheren Speicher und wurde nicht gesichert. Gib ihn in den KI-Einstellungen erneut ein, um den Coach auf diesem Telefon zu nutzen.",
"welcomeSettingsSkipped": "{n, plural, =1{1 Einstellung in der Sicherung war nicht lesbar und wurde übersprungen} other{{n} Einstellungen in der Sicherung waren nicht lesbar und wurden übersprungen}}",
"@welcomeSettingsSkipped": {
Expand Down Expand Up @@ -3685,5 +3712,7 @@
"stepCalDoneHousehold": "Gespeichert. Dein Smartphone hat {count} der {band} Schritte gezählt, die das Band erkannt hat. Bewegung im Haushalt wird jetzt auf {gain} skaliert.",
"whoopFirmwareWarningTitle": "WHOOP-Nutzer: Verbinde dich nicht wieder mit der WHOOP-App",
"whoopFirmwareWarningAction": "Wenn du OpenStrap oder eine andere Open-Source-App nutzt oder später zu einer wechseln möchtest, verbinde dein Band nicht wieder mit der offiziellen WHOOP-App. Sie kann Firmware-Updates automatisch installieren. Deinstalliere oder deaktiviere die WHOOP-App, damit sie dein Band nicht aktualisiert.",
"whoopFirmwareWarningDetails": "Berichte zur WHOOP 5.0 / MG Firmware 50.43.1.0 und 50.43.2.0 beschreiben einen blockierten Datenzugriff. Bluetooth kann sich weiterhin verbinden. OpenStrap kann das von WHOOP signierte Mitgliedschafts-Token nicht ausstellen, das betroffene Bänder entsperrt. Wir arbeiten an einer Lösung, aber es gibt noch keine Abhilfe."
"whoopFirmwareWarningDetails": "Berichte zur WHOOP 5.0 / MG Firmware 50.43.1.0 und 50.43.2.0 beschreiben einen blockierten Datenzugriff. Bluetooth kann sich weiterhin verbinden. OpenStrap kann das von WHOOP signierte Mitgliedschafts-Token nicht ausstellen, das betroffene Bänder entsperrt. Wir arbeiten an einer Lösung, aber es gibt noch keine Abhilfe.",
"pairSearchDevices": "Nach Geräten suchen",
"pairPrimaryCannotBeSensor": "Dies ist bereits dein Hauptarmband. Es kann nicht zusätzlich als Trainingssensor gekoppelt werden."
}
137 changes: 136 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -16765,6 +16765,139 @@
"@pairOuraExistingKeyGuidance": {
"description": "Before any Oura reset steps: existing-key users keep the ring paired in Oura and skip resets; reset only for a new key."
},
"pairAddDevice": "Add a device",
"@pairAddDevice": {
"description": "Label of the button that starts adding a device (band or sensor is decided after the scan)."
},
"pairKindAll": "All",
"@pairKindAll": {
"description": "Filter pill that shows every device in the list."
},
"pairKindStrap": "Chest straps & armbands",
"@pairKindStrap": {
"description": "Filter pill: heart-rate straps and armbands."
},
"pairKindRing": "Rings",
"@pairKindRing": {
"description": "Filter pill for smart rings."
},
"pairKindWatch": "Watches",
"@pairKindWatch": {
"description": "Filter pill for watches."
},
"pairGoBack": "Go back",
"@pairGoBack": {
"description": "Link that closes a confirm sheet without pairing."
},
"pairAddDeviceSub": "Rings, watches and heart-rate straps",
"@pairAddDeviceSub": {
"description": "Line under the Add a device row in Settings."
},
"pairAddDeviceSubtitle": "Each device shows where it fits: every day, or workouts only.",
"@pairAddDeviceSubtitle": {
"description": "Lead of the Add a device screen."
},
"pairBandFoundBody": "It does much more every day than during workouts only. Which do you want?",
"@pairBandFoundBody": {
"description": "WHOOP sheet lead when no band is paired yet."
},
"pairBandFoundBodyHasBand": "You already have a band. Only one can be your band at a time.",
"@pairBandFoundBodyHasBand": {
"description": "WHOOP sheet lead when a band is already paired."
},
"pairIdentifiedBody": "It looked like a heart-rate strap, but when we connected it identified itself as {label}. Nothing was saved.",
"@pairIdentifiedBody": {
"description": "Body of the sheet shown when a device picked as a sensor identifies itself as a band after connecting. Nothing was saved.",
"placeholders": {
"label": {
"type": "String"
}
}
},
"pairAsMyBand": "Pair it as my band",
"@pairAsMyBand": {
"description": "Button that pairs the device as the user’s main band."
},
"pairSecondaryPaired": "Paired for workouts only.",
"@pairSecondaryPaired": {
"description": "Shown after a device is paired for workouts only."
},
"pairBandNow": "Your band now: {name}. Your data stays when you switch.",
"@pairBandNow": {
"description": "Note under the table when a band is already paired. {name} is that band’s name.",
"placeholders": {
"name": {
"type": "String"
}
}
},
"pairReplaceBand": "Pair this band, then disconnect {name}",
"@pairReplaceBand": {
"description": "Main button when a band is already paired: unpair it and pair the tapped one. {name} is the paired band’s name.",
"placeholders": {
"name": {
"type": "String"
}
}
},
"pairReplaceConfirmTitle": "Replace {name} after this band connects?",
"@pairReplaceConfirmTitle": {
"description": "Confirm dialog before replacing the band. {name} is the paired band’s name.",
"placeholders": {
"name": {
"type": "String"
}
}
},
"pairReplaceConfirmBody": "Your current band stays paired if setup fails or you cancel. Your data stays.",
"@pairReplaceConfirmBody": {
"description": "Confirm dialog body: replacing the band keeps the data."
},
"pairReplaceConfirmYes": "Pair replacement",
"@pairReplaceConfirmYes": {
"description": "Confirm dialog button: unpair the old band and pair the new one."
},
"pairCurrentBandFallback": "your band",
"@pairCurrentBandFallback": {
"description": "Stands in for the paired band’s name when it has none, inside a sentence."
},
"pairWearableLead": "It can fill your days with sleep, recovery and heart rate.",
"@pairWearableLead": {
"description": "Sheet lead for a ring or watch."
},
"pairWearableNote": "Fills days without your WHOOP. Experimental, so switch it on in Devices.",
"@pairWearableNote": {
"description": "Note under a ring’s or watch’s table on the pairing sheet. “Devices” is the device list in Settings."
},
"pairStrapLead": "It adds heart rate while you work out. It does not record sleep, recovery or daily strain.",
"@pairStrapLead": {
"description": "Sheet lead for a heart-rate strap or sensor."
},
"pairStrapNote": "Both need “Use during workouts” on.",
"@pairStrapNote": {
"description": "Note under a strap’s table: both workout rows need the switch. Quotes deviceUseWorkoutsTitle."
},
"pairDeviceNamed": "Pair {name}",
"@pairDeviceNamed": {
"description": "Main button on a ring’s, watch’s or strap’s sheet. {name} is the device.",
"placeholders": {
"name": {
"type": "String"
}
}
},
"pairUseForWorkouts": "Use it for workouts only",
"@pairUseForWorkouts": {
"description": "Text link on a WHOOP sheet: keep it for workouts only, not as the main band."
},
"pairReplaceUnpairFailed": "Could not unpair your band, so the new one was not paired. Try again.",
"@pairReplaceUnpairFailed": {
"description": "Shown when unpairing the old band failed during “Unpair (your band) and pair this one”; nothing else ran."
},
"pairWearableLeadPebble": "Pebble 2 can fill your days with sleep, recovery and heart rate. Pebble 2 SE shows the watch’s own sleep and steps. It has no heart-rate sensor, so this app cannot calculate daily strain or estimate recovery from it.",
"@pairWearableLeadPebble": {
"description": "Pebble-family pairing lead: SE shows the watch’s own sleep and steps; no HR sensor means no app-calculated daily strain or recovery estimate from the SE alone."
},
"welcomeCoachKeyReentry": "Your coach API key stays in secure storage and was not backed up. Enter it again in AI settings to use the coach on this phone.",
"welcomeSettingsSkipped": "{n, plural, =1{1 setting in the backup could not be read and was skipped} other{{n} settings in the backup could not be read and were skipped}}",
"@welcomeSettingsSkipped": {
Expand Down Expand Up @@ -16805,5 +16938,7 @@
"@strengthSaveFailed": {"description": "Strength set editor copy."},
"napAutoLogTitle": "Log clear naps automatically",
"napAutoLogSub": "A detected nap with strong evidence is added without asking. You can still edit or remove it",
"napAutoLogSaveFailed": "That setting could not be saved. Try again."
"napAutoLogSaveFailed": "That setting could not be saved. Try again.",
"pairSearchDevices": "Search for devices",
"pairPrimaryCannotBeSensor": "This is already your main band. It cannot also be paired as a workout sensor."
}
31 changes: 30 additions & 1 deletion lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3063,6 +3063,33 @@
"pairModeTitle": "Ponlo en modo de emparejamiento",
"pairModeNoSite": "Un dispositivo genérico sin un único fabricante, así que no hay pasos del fabricante.",
"pairOuraExistingKeyGuidance": "¿Ya usas la app de Oura? Mantén el anillo en esa app y emparéjalo con la clave que ya tiene. Omite los pasos de restablecimiento de abajo. Restablécelo solo si quieres una clave nueva.",
"pairAddDevice": "Añadir un dispositivo",
"pairKindAll": "Todos",
"pairKindStrap": "Bandas de pecho y brazo",
"pairKindRing": "Anillos",
"pairKindWatch": "Relojes",
"pairGoBack": "Volver",
"pairAddDeviceSub": "Anillos, relojes y bandas de frecuencia cardiaca",
"pairAddDeviceSubtitle": "Cada dispositivo muestra dónde encaja: todos los días o solo en entrenamientos.",
"pairBandFoundBody": "Hace mucho más todos los días que solo en los entrenamientos. ¿Qué prefieres?",
"pairBandFoundBodyHasBand": "Ya tienes una pulsera. Solo una puede ser tu pulsera a la vez.",
"pairIdentifiedBody": "Parecía una banda de frecuencia cardíaca, pero al conectar se identificó como {label}. No se guardó nada.",
"pairAsMyBand": "Emparejar como mi pulsera",
"pairSecondaryPaired": "Emparejado solo para entrenamientos.",
"pairBandNow": "Tu pulsera ahora: {name}. Tus datos se conservan al cambiar.",
"pairReplaceBand": "Vincular esta pulsera y después desconectar {name}",
"pairReplaceConfirmTitle": "¿Reemplazar {name} después de conectar esta pulsera?",
"pairReplaceConfirmBody": "Tu pulsera actual seguirá vinculada si falla la configuración o cancelas. Tus datos se conservan.",
"pairReplaceConfirmYes": "Vincular reemplazo",
"pairCurrentBandFallback": "tu pulsera",
"pairWearableLead": "Puede completar tus días con sueño, recuperación y frecuencia cardiaca.",
"pairWearableNote": "Completa los días sin tu WHOOP. Es experimental, así que actívalo en Dispositivos.",
"pairStrapLead": "Añade la frecuencia cardiaca mientras entrenas. No registra sueño, recuperación ni esfuerzo diario.",
"pairStrapNote": "Ambos necesitan «Usar durante los entrenamientos» activado.",
"pairDeviceNamed": "Emparejar {name}",
"pairUseForWorkouts": "Usarla solo para entrenamientos",
"pairReplaceUnpairFailed": "No se pudo desemparejar tu pulsera, así que no se emparejó la nueva. Inténtalo de nuevo.",
"pairWearableLeadPebble": "Pebble 2 puede completar tus días con sueño, recuperación y frecuencia cardiaca. Pebble 2 SE muestra el sueño y los pasos registrados por el propio reloj. No tiene sensor de frecuencia cardiaca, por lo que esta app no puede calcular la carga diaria ni estimar la recuperación a partir de sus datos.",
"alarmNextLabel": "SIGUIENTE",
"alarmScheduleGroup": "Horario semanal",
"alarmWakeTimeRowTitle": "Hora de despertar",
Expand Down Expand Up @@ -3385,5 +3412,7 @@
"stepCalDoneHousehold": "Guardado. Tu teléfono contó {count} de los {band} pasos que vio la banda. El movimiento en casa ahora se escala a {gain}.",
"whoopFirmwareWarningTitle": "Usuarios de WHOOP: no vuelvas a conectarte a la app de WHOOP",
"whoopFirmwareWarningAction": "Si usas OpenStrap u otra app de código abierto, o piensas cambiarte a una más adelante, no vuelvas a conectar tu banda a la app oficial de WHOOP. Puede instalar actualizaciones de firmware automáticamente. Desinstala o desactiva la app de WHOOP para evitar que actualice tu banda.",
"whoopFirmwareWarningDetails": "Hay informes de que el firmware 50.43.1.0 y 50.43.2.0 de WHOOP 5.0 / MG bloquea el acceso a los datos. Es posible que el Bluetooth siga conectándose. OpenStrap no puede emitir el token de membresía firmado por WHOOP que se necesita para desbloquear las bandas afectadas. Estamos trabajando en una solución, pero todavía no hay ningún arreglo disponible."
"whoopFirmwareWarningDetails": "Hay informes de que el firmware 50.43.1.0 y 50.43.2.0 de WHOOP 5.0 / MG bloquea el acceso a los datos. Es posible que el Bluetooth siga conectándose. OpenStrap no puede emitir el token de membresía firmado por WHOOP que se necesita para desbloquear las bandas afectadas. Estamos trabajando en una solución, pero todavía no hay ningún arreglo disponible.",
"pairSearchDevices": "Buscar dispositivos",
"pairPrimaryCannotBeSensor": "Esta ya es tu pulsera principal. No se puede emparejar también como sensor de entrenamiento."
}
Loading
Loading