diff --git a/messages/de.json b/messages/de.json
index ad9268086..808caf400 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -4672,7 +4672,9 @@
"description": "Füge einen Ort und einen Zeitraum hinzu – eine Reise oder einen früheren Abschnitt deiner Verlaufsdaten. Der Zeitraum überschreibt für seine Tage deinen Wohnort, damit das Wetter zu deinem tatsächlichen Aufenthalt passt. So korrigierst du die Vergangenheit.",
"none": "Noch keine Ortszeiträume.",
"add": "{label} hinzufügen",
- "remove": "Ortszeitraum entfernen"
+ "remove": "Ortszeitraum entfernen",
+ "removeConfirmTitle": "Diese Reise entfernen?",
+ "removeConfirmBody": "Messwerte aus diesem Zeitraum bleiben erhalten; nur der Reise-Eintrag verschwindet."
},
"backfill": {
"title": "Vergangene Tage nacherfassen",
@@ -4953,7 +4955,10 @@
"surfaced": "Angezeigt",
"done": "Erledigt",
"dismissed": "Verworfen"
- }
+ },
+ "deleteConfirmTitle": "Diese Erinnerung löschen?",
+ "deleteConfirmBody": "Sie wird nicht mehr ausgelöst. An bereits erfassten Daten ändert sich nichts.",
+ "deleteConfirmAction": "Löschen"
},
"aboutMe": {
"title": "Über mich",
@@ -5628,8 +5633,16 @@
"revokeAllDone": "{count} vertrauenswürdige(s) Gerät(e) entfernt.",
"unnamed": "Unbekanntes Gerät",
"current": "Dieses Gerät",
- "expires": "Vertraut bis {date}"
- }
+ "expires": "Vertraut bis {date}",
+ "revokeConfirmTitle": "Diesem Gerät nicht mehr vertrauen?",
+ "revokeConfirmBody": "Es fragt beim nächsten Anmelden wieder nach deinem zweiten Faktor.",
+ "revokeAllConfirmTitle": "Keinem Gerät mehr vertrauen?",
+ "revokeAllConfirmBody": "Jedes Gerät fragt beim nächsten Anmelden wieder nach deinem zweiten Faktor, auch dieses."
+ },
+ "revokeSessionConfirmTitle": "Dieses Gerät abmelden?",
+ "revokeSessionConfirmBody": "{device} muss sich neu anmelden. An den dort erfassten Daten ändert sich nichts.",
+ "signOutEverywhereConfirmTitle": "Alle anderen Geräte abmelden?",
+ "signOutEverywhereConfirmBody": "Jedes Gerät außer diesem muss sich neu anmelden. Dieses Gerät bleibt angemeldet. An deinen erfassten Daten ändert sich nichts."
},
"privacy": {
"intro": {
diff --git a/messages/en.json b/messages/en.json
index c78f6c78c..98c2df8bf 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -4672,7 +4672,9 @@
"description": "Add a place and a date range — a trip, or a stretch of earlier history. The period overrides your home for the days it covers, so weather matches where you actually were. This is how you correct the past.",
"none": "No location periods yet.",
"add": "Add {label}",
- "remove": "Remove location period"
+ "remove": "Remove location period",
+ "removeConfirmTitle": "Remove this trip?",
+ "removeConfirmBody": "Readings taken during it stay in your record; only the trip entry goes."
},
"backfill": {
"title": "Backfill past days",
@@ -4953,7 +4955,10 @@
"surfaced": "Surfaced",
"done": "Done",
"dismissed": "Dismissed"
- }
+ },
+ "deleteConfirmTitle": "Delete this reminder?",
+ "deleteConfirmBody": "It stops being raised. Anything you have already recorded is untouched.",
+ "deleteConfirmAction": "Delete"
},
"aboutMe": {
"title": "About me",
@@ -5628,8 +5633,16 @@
"revokeAllDone": "Forgot {count} trusted device(s).",
"unnamed": "Unknown device",
"current": "This device",
- "expires": "Trusted until {date}"
- }
+ "expires": "Trusted until {date}",
+ "revokeConfirmTitle": "Stop trusting this device?",
+ "revokeConfirmBody": "It will ask for your second factor again the next time you sign in from it.",
+ "revokeAllConfirmTitle": "Stop trusting every device?",
+ "revokeAllConfirmBody": "Every device will ask for your second factor again the next time you sign in from it, including this one."
+ },
+ "revokeSessionConfirmTitle": "Sign this device out?",
+ "revokeSessionConfirmBody": "{device} will need to sign in again. Nothing recorded on it is affected.",
+ "signOutEverywhereConfirmTitle": "Sign out every other device?",
+ "signOutEverywhereConfirmBody": "Every device except this one will need to sign in again. This device stays signed in. Nothing you have recorded is affected."
},
"privacy": {
"intro": {
diff --git a/messages/es.json b/messages/es.json
index 7e77e5ed2..895108146 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -4672,7 +4672,9 @@
"description": "Añade un lugar y un rango de fechas: un viaje o un tramo de tu historial anterior. El periodo anula tu hogar en los días que cubre, para que el tiempo coincida con donde estuviste realmente. Así corriges el pasado.",
"none": "Aún no hay periodos de ubicación.",
"add": "Añadir {label}",
- "remove": "Eliminar periodo de ubicación"
+ "remove": "Eliminar periodo de ubicación",
+ "removeConfirmTitle": "¿Quitar este viaje?",
+ "removeConfirmBody": "Las mediciones de ese periodo permanecen; solo desaparece la entrada del viaje."
},
"backfill": {
"title": "Rellenar días pasados",
@@ -4953,7 +4955,10 @@
"surfaced": "Mostrado",
"done": "Hecho",
"dismissed": "Descartado"
- }
+ },
+ "deleteConfirmTitle": "¿Eliminar este recordatorio?",
+ "deleteConfirmBody": "Deja de aparecer. Lo que ya has registrado no cambia.",
+ "deleteConfirmAction": "Eliminar"
},
"aboutMe": {
"title": "Sobre mí",
@@ -5628,8 +5633,16 @@
"revokeAllDone": "Se olvidaron {count} dispositivo(s) de confianza.",
"unnamed": "Dispositivo desconocido",
"current": "Este dispositivo",
- "expires": "De confianza hasta el {date}"
- }
+ "expires": "De confianza hasta el {date}",
+ "revokeConfirmTitle": "¿Dejar de confiar en este dispositivo?",
+ "revokeConfirmBody": "Volverá a pedir tu segundo factor la próxima vez que inicies sesión desde él.",
+ "revokeAllConfirmTitle": "¿Dejar de confiar en todos los dispositivos?",
+ "revokeAllConfirmBody": "Todos los dispositivos volverán a pedir tu segundo factor la próxima vez que inicies sesión, incluido este."
+ },
+ "revokeSessionConfirmTitle": "¿Cerrar la sesión de este dispositivo?",
+ "revokeSessionConfirmBody": "{device} tendrá que iniciar sesión de nuevo. Lo registrado en él no se ve afectado.",
+ "signOutEverywhereConfirmTitle": "¿Cerrar la sesión en los demás dispositivos?",
+ "signOutEverywhereConfirmBody": "Todos los dispositivos salvo este tendrán que iniciar sesión de nuevo. Este seguirá con la sesión abierta. Nada de lo que has registrado se ve afectado."
},
"privacy": {
"intro": {
diff --git a/messages/fr.json b/messages/fr.json
index a390a4c5f..fdf293245 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -4672,7 +4672,9 @@
"description": "Ajoutez un lieu et une plage de dates : un voyage ou une portion de votre historique passé. La période remplace votre domicile pour les jours qu'elle couvre, afin que la météo corresponde à l'endroit où vous étiez réellement. C'est ainsi que vous corrigez le passé.",
"none": "Aucune période de localisation pour l'instant.",
"add": "Ajouter {label}",
- "remove": "Supprimer la période de localisation"
+ "remove": "Supprimer la période de localisation",
+ "removeConfirmTitle": "Retirer ce voyage ?",
+ "removeConfirmBody": "Les mesures de cette période restent dans ton dossier ; seule l'entrée du voyage disparaît."
},
"backfill": {
"title": "Récupérer les jours passés",
@@ -4953,7 +4955,10 @@
"surfaced": "Affiché",
"done": "Terminé",
"dismissed": "Ignoré"
- }
+ },
+ "deleteConfirmTitle": "Supprimer ce rappel ?",
+ "deleteConfirmBody": "Il ne se déclenchera plus. Ce que tu as déjà enregistré reste intact.",
+ "deleteConfirmAction": "Supprimer"
},
"aboutMe": {
"title": "À propos de moi",
@@ -5628,8 +5633,16 @@
"revokeAllDone": "{count} appareil(s) de confiance oublié(s).",
"unnamed": "Appareil inconnu",
"current": "Cet appareil",
- "expires": "De confiance jusqu'au {date}"
- }
+ "expires": "De confiance jusqu'au {date}",
+ "revokeConfirmTitle": "Ne plus faire confiance à cet appareil ?",
+ "revokeConfirmBody": "Il redemandera ton deuxième facteur à la prochaine connexion depuis cet appareil.",
+ "revokeAllConfirmTitle": "Ne plus faire confiance à aucun appareil ?",
+ "revokeAllConfirmBody": "Chaque appareil redemandera ton deuxième facteur à la prochaine connexion, celui-ci compris."
+ },
+ "revokeSessionConfirmTitle": "Déconnecter cet appareil ?",
+ "revokeSessionConfirmBody": "{device} devra se reconnecter. Ce qui y est enregistré n'est pas affecté.",
+ "signOutEverywhereConfirmTitle": "Déconnecter tous les autres appareils ?",
+ "signOutEverywhereConfirmBody": "Chaque appareil sauf celui-ci devra se reconnecter. Celui-ci reste connecté. Rien de ce que tu as enregistré n'est affecté."
},
"privacy": {
"intro": {
diff --git a/messages/it.json b/messages/it.json
index 827c43c5f..51fe88723 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -4672,7 +4672,9 @@
"description": "Aggiungi un luogo e un intervallo di date: un viaggio o un tratto della tua cronologia passata. Il periodo prevale sulla tua residenza nei giorni che copre, così il meteo corrisponde a dove ti trovavi davvero. È così che correggi il passato.",
"none": "Nessun periodo di posizione.",
"add": "Aggiungi {label}",
- "remove": "Rimuovi periodo di posizione"
+ "remove": "Rimuovi periodo di posizione",
+ "removeConfirmTitle": "Rimuovere questo viaggio?",
+ "removeConfirmBody": "Le misurazioni di quel periodo restano nel tuo archivio; sparisce solo la voce del viaggio."
},
"backfill": {
"title": "Recupera i giorni passati",
@@ -4953,7 +4955,10 @@
"surfaced": "Mostrato",
"done": "Fatto",
"dismissed": "Ignorato"
- }
+ },
+ "deleteConfirmTitle": "Eliminare questo promemoria?",
+ "deleteConfirmBody": "Non verrà più mostrato. Quanto hai già registrato resta invariato.",
+ "deleteConfirmAction": "Elimina"
},
"aboutMe": {
"title": "Su di me",
@@ -5628,8 +5633,16 @@
"revokeAllDone": "Dimenticati {count} dispositivo/i attendibile/i.",
"unnamed": "Dispositivo sconosciuto",
"current": "Questo dispositivo",
- "expires": "Attendibile fino al {date}"
- }
+ "expires": "Attendibile fino al {date}",
+ "revokeConfirmTitle": "Non fidarsi più di questo dispositivo?",
+ "revokeConfirmBody": "Al prossimo accesso da questo dispositivo chiederà di nuovo il secondo fattore.",
+ "revokeAllConfirmTitle": "Non fidarsi più di alcun dispositivo?",
+ "revokeAllConfirmBody": "Ogni dispositivo chiederà di nuovo il secondo fattore al prossimo accesso, questo compreso."
+ },
+ "revokeSessionConfirmTitle": "Disconnettere questo dispositivo?",
+ "revokeSessionConfirmBody": "{device} dovrà accedere di nuovo. Quanto registrato su di esso non viene toccato.",
+ "signOutEverywhereConfirmTitle": "Disconnettere tutti gli altri dispositivi?",
+ "signOutEverywhereConfirmBody": "Ogni dispositivo tranne questo dovrà accedere di nuovo. Questo resta connesso. Nulla di ciò che hai registrato viene toccato."
},
"privacy": {
"intro": {
diff --git a/messages/pl.json b/messages/pl.json
index a43e94b7f..ef361115b 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -4672,7 +4672,9 @@
"description": "Dodaj miejsce i zakres dat: podróż lub fragment wcześniejszej historii. Okres zastępuje Twój dom w dniach, które obejmuje, aby pogoda odpowiadała miejscu, w którym naprawdę byłeś. W ten sposób korygujesz przeszłość.",
"none": "Brak okresów lokalizacji.",
"add": "Dodaj {label}",
- "remove": "Usuń okres lokalizacji"
+ "remove": "Usuń okres lokalizacji",
+ "removeConfirmTitle": "Usunąć tę podróż?",
+ "removeConfirmBody": "Pomiary z tego okresu pozostają w historii; znika tylko wpis podróży."
},
"backfill": {
"title": "Uzupełnij minione dni",
@@ -4953,7 +4955,10 @@
"surfaced": "Wyświetlone",
"done": "Gotowe",
"dismissed": "Odrzucone"
- }
+ },
+ "deleteConfirmTitle": "Usunąć to przypomnienie?",
+ "deleteConfirmBody": "Przestanie się pojawiać. Zapisane wcześniej dane pozostają bez zmian.",
+ "deleteConfirmAction": "Usuń"
},
"aboutMe": {
"title": "O mnie",
@@ -5628,8 +5633,16 @@
"revokeAllDone": "Zapomniano {count} zaufane urządzenie/a.",
"unnamed": "Nieznane urządzenie",
"current": "To urządzenie",
- "expires": "Zaufane do {date}"
- }
+ "expires": "Zaufane do {date}",
+ "revokeConfirmTitle": "Przestać ufać temu urządzeniu?",
+ "revokeConfirmBody": "Przy następnym logowaniu z tego urządzenia ponownie poprosi o drugi składnik.",
+ "revokeAllConfirmTitle": "Przestać ufać wszystkim urządzeniom?",
+ "revokeAllConfirmBody": "Każde urządzenie przy następnym logowaniu ponownie poprosi o drugi składnik, łącznie z tym."
+ },
+ "revokeSessionConfirmTitle": "Wylogować to urządzenie?",
+ "revokeSessionConfirmBody": "{device} będzie musiało zalogować się ponownie. Zapisane na nim dane pozostają bez zmian.",
+ "signOutEverywhereConfirmTitle": "Wylogować wszystkie pozostałe urządzenia?",
+ "signOutEverywhereConfirmBody": "Każde urządzenie poza tym będzie musiało zalogować się ponownie. To urządzenie pozostaje zalogowane. Zapisane dane pozostają bez zmian."
},
"privacy": {
"intro": {
diff --git a/src/components/settings/__tests__/destructive-controls-confirm.test.tsx b/src/components/settings/__tests__/destructive-controls-confirm.test.tsx
new file mode 100644
index 000000000..6d5b0d96c
--- /dev/null
+++ b/src/components/settings/__tests__/destructive-controls-confirm.test.tsx
@@ -0,0 +1,67 @@
+import { describe, it, expect } from "vitest";
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+/**
+ * Destructive settings controls ask first.
+ *
+ * Signing every other device out, and revoking a trusted device, used to fire
+ * on a single tap. Neither is undoable — the other devices simply have to log
+ * in again, and a revoked trusted device asks for a second factor next time —
+ * while every other destructive surface in the app confirms. On a shared
+ * screen a mis-tap is a support call.
+ *
+ * This reads the sources rather than rendering, because what it pins is
+ * structural: that these particular controls route through `ConfirmButton`
+ * and never call a revoke mutation straight from an `onClick`. A render test
+ * would prove the dialog exists in one arrangement; this proves no control on
+ * these cards can go back to firing directly.
+ */
+
+const CARDS = [
+ {
+ file: "security-sessions-card.tsx",
+ slots: ["revoke-session", "sign-out-everywhere"],
+ },
+ {
+ file: "trusted-devices-card.tsx",
+ slots: ["revoke-trusted-device", "revoke-all-trusted-devices"],
+ },
+];
+
+function source(file: string) {
+ return readFileSync(
+ join(process.cwd(), "src", "components", "settings", file),
+ "utf8",
+ );
+}
+
+describe("destructive session controls confirm before firing", () => {
+ for (const card of CARDS) {
+ describe(card.file, () => {
+ const src = source(card.file);
+
+ it("routes every destructive control through ConfirmButton", () => {
+ expect(src).toContain("ConfirmButton");
+ for (const slot of card.slots) {
+ expect(src).toContain(`slot="${slot}"`);
+ }
+ });
+
+ it("never fires a revoke straight from an onClick", () => {
+ // `onConfirm={() => revoke…}` is the confirmed path and is fine;
+ // `onClick={() => revoke…}` is the unguarded one this forbids.
+ const unguarded = src.match(/onClick=\{\(\) =>\s*revoke/g) ?? [];
+ expect(unguarded).toEqual([]);
+ });
+
+ it("gives each control its own confirmation body", () => {
+ // A shared generic body would let one control's dialog describe
+ // another's consequence — the reason the copy is per-control.
+ const bodies = src.match(/body=\{t\("([^"]+)"/g) ?? [];
+ expect(bodies.length).toBe(card.slots.length);
+ expect(new Set(bodies).size).toBe(card.slots.length);
+ });
+ });
+ }
+});
diff --git a/src/components/settings/coach-reminders-section.tsx b/src/components/settings/coach-reminders-section.tsx
index f00e2a5da..c8eab020b 100644
--- a/src/components/settings/coach-reminders-section.tsx
+++ b/src/components/settings/coach-reminders-section.tsx
@@ -18,13 +18,14 @@
* Coach surface like the rest of the memory controls.
*/
import { useMemo } from "react";
-import { BellRing, Check, Loader2, Trash2, X } from "lucide-react";
+import { BellRing, Check, Trash2, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { formatDateOrRelative } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
+import { ConfirmButton } from "@/components/settings/confirm-button";
import { useTranslations } from "@/lib/i18n/context";
import {
useCoachReminders,
@@ -129,25 +130,21 @@ export function CoachRemindersSection({
{t("settings.ai.coachReminders.dismiss")}
)}
-
+ disabled={!isAuthenticated}
+ pending={busy}
+ ariaLabel={t("settings.ai.coachReminders.deleteAria")}
+ label=""
+ icon={