feat(mobile): show OTA update id next to app version

Meine Tiere tab footer now shows 'Version X.X.X - OTA <short-id>' (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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-19 11:05:26 +02:00
1 parent 7187fedb38
commit 8b1aba1d9f
3 files changed
+40 -4

No files matched your search

+32 -2
View File
@@ -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() {
<Text style={styles.legalLink}>{link.label}</Text>
</Pressable>
))}
<Text style={styles.versionText}>{t("pets.versionLabel", { version })}</Text>
<Text style={styles.versionText}>{t("pets.versionLabel", { version, ota })}</Text>
</View>
);
}
+4 -1
View File
@@ -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."
+4 -1
View File
@@ -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."