From 804d4a586166e0c83fcded8f3b1344a960263d4e Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Fri, 15 May 2026 21:16:22 +0200 Subject: [PATCH] feat(native): device-info api headers + DeviceLimitSheet UI + profile i18n sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/api.ts: sends x-device-name + x-device-model + x-device-os headers (cached per session, URL-encoded). Backend persists into user_devices for visual differentiation in DeviceLimitSheet. - DeviceLimitReachedSheet: renders name (primary) + model · OS-version (secondary), "Dieses Gerät"-Pill on isCurrent. Stale phantoms become distinguishable. - Profile i18n sweep: 8 keys × 3 languages = 24 fixes — all {{var}} placeholders switched to %{var} matching i18next config (Vue-i18n leftover from Nuxt-port). Co-Authored-By: Claude Opus 4.7 --- .../components/DeviceLimitReachedSheet.tsx | 58 ++++++++++++++----- apps/rebreak-native/lib/api.ts | 25 ++++++-- apps/rebreak-native/locales/de.json | 19 +++--- apps/rebreak-native/locales/en.json | 19 +++--- apps/rebreak-native/locales/fr.json | 19 +++--- apps/rebreak-native/stores/deviceLimit.ts | 2 + 6 files changed, 96 insertions(+), 46 deletions(-) diff --git a/apps/rebreak-native/components/DeviceLimitReachedSheet.tsx b/apps/rebreak-native/components/DeviceLimitReachedSheet.tsx index 08dba52..b50607c 100644 --- a/apps/rebreak-native/components/DeviceLimitReachedSheet.tsx +++ b/apps/rebreak-native/components/DeviceLimitReachedSheet.tsx @@ -42,6 +42,16 @@ function DeviceLimitRow({ const { t } = useTranslation(); const colors = useColors(); + const primaryName = device.name ?? device.model ?? device.platform; + const secondaryParts: string[] = []; + if (device.model) secondaryParts.push(device.model); + if (device.osVersion) { + const osLabel = device.platform === 'android' ? 'Android' : 'iOS'; + secondaryParts.push(`${osLabel} ${device.osVersion}`); + } + const secondaryLine = secondaryParts.join(' · '); + const showSecondary = secondaryLine.length > 0 && secondaryLine !== primaryName; + return ( - - {device.name ?? device.model ?? device.platform} - - {device.model && device.name && !device.name.includes(device.model) ? ( + + + {primaryName} + + {device.isCurrent ? ( + + + {t('device_limit.this_device')} + + + ) : null} + + {showSecondary ? ( - {device.model} + {secondaryLine} ) : null} diff --git a/apps/rebreak-native/lib/api.ts b/apps/rebreak-native/lib/api.ts index 8d73b0d..5923523 100644 --- a/apps/rebreak-native/lib/api.ts +++ b/apps/rebreak-native/lib/api.ts @@ -1,6 +1,6 @@ import Constants from 'expo-constants'; import { supabase } from './supabase'; -import { getDeviceId, getPlatformName } from './deviceId'; +import { getDeviceInfo } from './deviceId'; import { useDeviceLimitStore } from '../stores/deviceLimit'; const apiUrl = Constants.expoConfig?.extra?.apiUrl as string; @@ -11,6 +11,22 @@ type FetchOptions = Omit & { skipDeviceHeader?: boolean; }; +let cachedDeviceHeaders: Record | null = null; + +async function getDeviceHeaders(): Promise> { + if (cachedDeviceHeaders) return cachedDeviceHeaders; + const info = await getDeviceInfo().catch(() => null); + if (!info) return {}; + cachedDeviceHeaders = { + 'x-device-id': info.deviceId, + 'x-platform': info.platform, + 'x-device-name': encodeURIComponent(info.name), + 'x-device-model': encodeURIComponent(info.model), + 'x-device-os': encodeURIComponent(info.osVersion), + }; + return cachedDeviceHeaders; +} + /** * Wrapper für Backend-API-Calls mit automatischem Auth-Token. * Pendant zum Nuxt-`useSafeFetch` aus apps/rebreak/. @@ -35,11 +51,8 @@ export async function apiFetch( } if (!skipDeviceHeader) { - const deviceId = await getDeviceId().catch(() => null); - if (deviceId) { - headers['x-device-id'] = deviceId; - headers['x-platform'] = getPlatformName(); - } + const deviceHeaders = await getDeviceHeaders(); + Object.assign(headers, deviceHeaders); } const res = await fetch(`${apiUrl}${path}`, { diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 2cee28f..fb99e61 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -589,7 +589,8 @@ "title": "Geräte-Limit erreicht", "subtitle": "%{count} von %{max} Geräten belegt (%{plan}) — entferne ein Gerät um weiterzumachen", "hint": "Entfernte Geräte können sich beim nächsten Login wieder registrieren.", - "remove_cta": "Gerät entfernen" + "remove_cta": "Gerät entfernen", + "this_device": "Dieses Gerät" }, "urge": { "title": "SOS — Atemübung", @@ -783,22 +784,22 @@ "crop_reset": "Zurücksetzen", "streak_section_label": "STREAK", "streak_days_protected": "Tage geschützt", - "streak_since": "seit {{date}}", - "streak_longest": "Längste Streak: {{days}} Tage", + "streak_since": "seit %{date}", + "streak_longest": "Längste Streak: %{days} Tage", "cooldown": { "heading": "COOLDOWN-VERLAUF", - "window_label": "letzte {{weeks}}W", - "week_label": "W{{n}}", + "window_label": "letzte %{weeks}W", + "week_label": "W%{n}", "none": "Keine Cooldowns in den letzten 8 Wochen", - "count_one": "1 Cooldown in {{weeks}} Wochen", - "count_other": "{{n}} Cooldowns in {{weeks}} Wochen", - "avg_last": "Ø 1 pro {{avg}} Wochen · zuletzt {{date}}", + "count_one": "1 Cooldown in %{weeks} Wochen", + "count_other": "%{n} Cooldowns in %{weeks} Wochen", + "avg_last": "Ø 1 pro %{avg} Wochen · zuletzt %{date}", "patterns": { "toggle_label": "Mehr Infos", "hour_heading": "Wann startest du Cooldowns?", "day_heading": "An welchen Tagen?", "reason_heading": "Häufige Begriffe", - "cancel_rate": "Cooldowns abgebrochen: {{pct}}%", + "cancel_rate": "Cooldowns abgebrochen: %{pct}%", "not_enough": "Noch keine Muster erkannt", "weekday_mon": "Mo", "weekday_tue": "Di", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index 3972bdb..6ca2388 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -589,7 +589,8 @@ "title": "Device limit reached", "subtitle": "%{count} of %{max} device slots used (%{plan}) — remove a device to continue", "hint": "Removed devices can re-register on next sign-in.", - "remove_cta": "Remove device" + "remove_cta": "Remove device", + "this_device": "This device" }, "urge": { "title": "SOS — Breathing exercise", @@ -783,22 +784,22 @@ "crop_reset": "Reset", "streak_section_label": "STREAK", "streak_days_protected": "days protected", - "streak_since": "since {{date}}", - "streak_longest": "Longest streak: {{days}} days", + "streak_since": "since %{date}", + "streak_longest": "Longest streak: %{days} days", "cooldown": { "heading": "COOLDOWN HISTORY", - "window_label": "last {{weeks}}W", - "week_label": "W{{n}}", + "window_label": "last %{weeks}W", + "week_label": "W%{n}", "none": "No cooldowns in the last 8 weeks", - "count_one": "1 cooldown over {{weeks}} weeks", - "count_other": "{{n}} cooldowns over {{weeks}} weeks", - "avg_last": "Ø 1 every {{avg}} weeks · last {{date}}", + "count_one": "1 cooldown over %{weeks} weeks", + "count_other": "%{n} cooldowns over %{weeks} weeks", + "avg_last": "Ø 1 every %{avg} weeks · last %{date}", "patterns": { "toggle_label": "More insights", "hour_heading": "When do you start cooldowns?", "day_heading": "Which days?", "reason_heading": "Common terms", - "cancel_rate": "Cooldowns cancelled: {{pct}}%", + "cancel_rate": "Cooldowns cancelled: %{pct}%", "not_enough": "Not enough patterns yet", "weekday_mon": "Mon", "weekday_tue": "Tue", diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json index 39fd80e..e2a4381 100644 --- a/apps/rebreak-native/locales/fr.json +++ b/apps/rebreak-native/locales/fr.json @@ -589,7 +589,8 @@ "title": "Limite d'appareils atteinte", "subtitle": "%{count} sur %{max} emplacements utilisés (%{plan}) — supprimez un appareil pour continuer", "hint": "Les appareils supprimés peuvent se réenregistrer à la prochaine connexion.", - "remove_cta": "Supprimer un appareil" + "remove_cta": "Supprimer un appareil", + "this_device": "Cet appareil" }, "urge": { "title": "SOS — Exercice de respiration", @@ -783,22 +784,22 @@ "crop_reset": "Réinitialiser", "streak_section_label": "SÉRIE", "streak_days_protected": "jours protégés", - "streak_since": "depuis {{date}}", - "streak_longest": "Série la plus longue : {{days}} jours", + "streak_since": "depuis %{date}", + "streak_longest": "Série la plus longue : %{days} jours", "cooldown": { "heading": "HISTORIQUE DES PAUSES", - "window_label": "{{weeks}} dernières sem.", - "week_label": "S{{n}}", + "window_label": "%{weeks} dernières sem.", + "week_label": "S%{n}", "none": "Aucune pause dans les 8 dernières semaines", - "count_one": "1 pause sur {{weeks}} semaines", - "count_other": "{{n}} pauses sur {{weeks}} semaines", - "avg_last": "Ø 1 toutes les {{avg}} semaines · dernière {{date}}", + "count_one": "1 pause sur %{weeks} semaines", + "count_other": "%{n} pauses sur %{weeks} semaines", + "avg_last": "Ø 1 toutes les %{avg} semaines · dernière %{date}", "patterns": { "toggle_label": "Plus d'infos", "hour_heading": "Quand démarrez-vous des pauses ?", "day_heading": "Quels jours ?", "reason_heading": "Termes fréquents", - "cancel_rate": "Pauses annulées : {{pct}}%", + "cancel_rate": "Pauses annulées : %{pct}%", "not_enough": "Pas encore assez de données", "weekday_mon": "Lun", "weekday_tue": "Mar", diff --git a/apps/rebreak-native/stores/deviceLimit.ts b/apps/rebreak-native/stores/deviceLimit.ts index f1e501b..b466933 100644 --- a/apps/rebreak-native/stores/deviceLimit.ts +++ b/apps/rebreak-native/stores/deviceLimit.ts @@ -6,8 +6,10 @@ export type DeviceLimitDevice = { platform: string; model: string | null; name: string | null; + osVersion: string | null; lastSeenAt: string; createdAt: string; + isCurrent?: boolean; }; type DeviceLimitState = {