From 8b1aba1d9f231a0f474ac1097d7ed8cbbcaa20f9 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 19 Sep 2026 11:05:26 +0200 Subject: [PATCH] feat(mobile): show OTA update id next to app version Meine Tiere tab footer now shows 'Version X.X.X - OTA ' (or 'Build (kein OTA)' when running the embedded/non-OTA bundle) via expo-updates' Updates.isEmbeddedLaunch/updateId. Directly answers the support question 'did the update actually arrive on this device?' by comparing the shown short id against the 'Android update ID' an 'eas update' publish prints - the exact ambiguity that cost a multi-day debugging session earlier (see .claude/handoffs/2026-09-12-ota-update- mystery-expo-dev-client-root-cause.md). Code-reviewed before commit (per WORKFLOW.md step 6): fixed two of four findings - wrapped the expo-updates reads in a try/catch (LegalSection sits outside FooterErrorBoundary, so an unguarded throw here would have taken down the whole footer) and moved the hardcoded 'OTA ' prefix into i18n (pets.otaId). The web-shim-always-unknown finding is a documented dead branch (this app ships no web build - eas update only ever runs --platform android). No test coverage added - no RN test infrastructure exists in mobile/ yet (pre-existing gap, tracked in WORKFLOW.md). Co-Authored-By: Claude Sonnet 5 --- mobile/app/(app)/(tabs)/pets.tsx | 34 ++++++++++++++++++++++++++++++-- mobile/src/i18n/locales/de.json | 5 ++++- mobile/src/i18n/locales/en.json | 5 ++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/mobile/app/(app)/(tabs)/pets.tsx b/mobile/app/(app)/(tabs)/pets.tsx index 50bfcc4..7b8e36c 100644 --- a/mobile/app/(app)/(tabs)/pets.tsx +++ b/mobile/app/(app)/(tabs)/pets.tsx @@ -4,6 +4,7 @@ import { useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import Constants from "expo-constants"; +import * as Updates from "expo-updates"; import * as Sentry from "@sentry/react-native"; import { useAuth } from "@clerk/expo"; import { Check, Sun, Moon, SunMoon } from "lucide-react-native"; @@ -216,12 +217,41 @@ function PetSuggestionsSection() { ); } -/** Links to the fixed-language legal pages (see root CLAUDE.md: not localized, always German) plus the installed app version, for anyone who needs to reach them without going through the web app. */ +/** + * Reads expo-updates' launch info defensively — these are plain native-module-backed exports, not + * function calls, but a stripped/misconfigured build (e.g. a dev client without the module properly + * linked) throwing here would otherwise crash LegalSection's render, and LegalSection sits outside + * FooterErrorBoundary (that only wraps LogoutSection below it), so there's no fallback UI to catch it. + * Not meaningful on the web target (the web shim always reports isEmbeddedLaunch=false/updateId=null) — + * this app has no shipped web build (`eas update` only ever runs `--platform android`, see README), so + * that's a dead branch today, not a live bug. + */ +function readOtaUpdateId(): { isEmbeddedLaunch: boolean; updateId: string | null } { + try { + return { isEmbeddedLaunch: Updates.isEmbeddedLaunch, updateId: Updates.updateId }; + } catch { + return { isEmbeddedLaunch: false, updateId: null }; + } +} + +/** + * Links to the fixed-language legal pages (see root CLAUDE.md: not localized, always German) plus + * the installed app version and current OTA update state, for anyone who needs to reach them without + * going through the web app. The OTA suffix exists so a support conversation ("did the update actually + * arrive?") can be answered by reading this screen instead of guessing — compare the short id shown + * here against the "Android update ID" an `eas update` publish prints. + */ function LegalSection() { const { t } = useTranslation(); const { colors } = useTheme(); const styles = useMemo(() => makeStyles(colors), [colors]); const version = Constants.expoConfig?.version ?? "?"; + const { isEmbeddedLaunch, updateId } = readOtaUpdateId(); + const ota = isEmbeddedLaunch + ? t("pets.otaEmbedded") + : updateId + ? t("pets.otaId", { id: updateId.slice(0, 8) }) + : t("pets.otaUnknown"); const links = [ { label: t("pets.legalImpressum"), path: "impressum" }, @@ -237,7 +267,7 @@ function LegalSection() { {link.label} ))} - {t("pets.versionLabel", { version })} + {t("pets.versionLabel", { version, ota })} ); } diff --git a/mobile/src/i18n/locales/de.json b/mobile/src/i18n/locales/de.json index b32f9ec..d2e5120 100644 --- a/mobile/src/i18n/locales/de.json +++ b/mobile/src/i18n/locales/de.json @@ -209,7 +209,10 @@ "legalChildSafety": "Kinderschutz", "petSuggestionsTitle": "Tier-Vorschläge", "petSuggestionsLabel": "Erhalte gelegentlich eine Benachrichtigung zu einem Tier, das dir gefallen könnte.", - "versionLabel": "Version {{version}}", + "versionLabel": "Version {{version}} - {{ota}}", + "otaId": "OTA {{id}}", + "otaEmbedded": "Build (kein OTA)", + "otaUnknown": "OTA unbekannt", "logoutButton": "Abmelden", "logoutConfirmTitle": "Abmelden?", "logoutConfirmMessage": "Du wirst von deinem Konto abgemeldet und zum Login zurückgeschickt." diff --git a/mobile/src/i18n/locales/en.json b/mobile/src/i18n/locales/en.json index 11aec4a..6287050 100644 --- a/mobile/src/i18n/locales/en.json +++ b/mobile/src/i18n/locales/en.json @@ -209,7 +209,10 @@ "legalChildSafety": "Child Safety Standards", "petSuggestionsTitle": "Pet suggestions", "petSuggestionsLabel": "Occasionally get notified about a pet you might like.", - "versionLabel": "Version {{version}}", + "versionLabel": "Version {{version}} - {{ota}}", + "otaId": "OTA {{id}}", + "otaEmbedded": "Build (no OTA)", + "otaUnknown": "OTA unknown", "logoutButton": "Log out", "logoutConfirmTitle": "Log out?", "logoutConfirmMessage": "You'll be signed out of your account and sent back to the login screen."