fix(mobile): sign-up link overflow, pre-login language switcher; set up EAS Update (1.0.2)

- Sign-up's Terms/Privacy links overflowed the screen width — they
  reused onboarding's long descriptive sentences ("Alle Details in
  unseren Nutzungsbedingungen") in a compact side-by-side row with no
  wrap. Switched to the existing short pets.legalTerms/legalPrivacy
  labels and added flexWrap as a safety net.
- Neither sign-in nor sign-up let you pick a language before logging
  in — extracted pets.tsx's language toggle into a shared
  LanguageSwitcher component and added it to both auth screens.
- Set up expo-updates (Gitea #35) via `eas update:configure`:
  runtimeVersion policy "appVersion", per-profile update channels in
  eas.json (development/preview/production). Future JS-only fixes can
  now ship via `eas update` without a new Play Store review — this
  build itself still needs a full native build since expo-updates is
  a native module. Manually removed a duplicate RECORD_AUDIO
  permission entry that update:configure's app.json rewrite introduced.
- Bumped to 1.0.2 (versionCode 3).
This commit is contained in:
2026-09-08 06:34:50 +02:00
parent b663892865
commit bad24e6463
8 changed files with 159 additions and 52 deletions
+9 -3
View File
@@ -3,7 +3,7 @@
"name": "PawFeed",
"slug": "pawfeed-mobile",
"scheme": "pawfeed",
"version": "1.0.1",
"version": "1.0.2",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
@@ -13,7 +13,7 @@
},
"android": {
"package": "org.pawfeed.app",
"versionCode": 2,
"versionCode": 3,
"googleServicesFile": "./google-services.json",
"adaptiveIcon": {
"backgroundColor": "#fafafa",
@@ -67,6 +67,12 @@
"projectId": "e56a076a-08a2-4e4e-afa0-dfc611fba536"
}
},
"owner": "pawfeed"
"owner": "pawfeed",
"runtimeVersion": {
"policy": "appVersion"
},
"updates": {
"url": "https://u.expo.dev/e56a076a-08a2-4e4e-afa0-dfc611fba536"
}
}
}
+4 -43
View File
@@ -1,17 +1,16 @@
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { View, Text, FlatList, Pressable, ActivityIndicator, Linking, StyleSheet } from "react-native";
import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import Constants from "expo-constants";
import { Check, Sun, Moon, SunMoon } from "lucide-react-native";
import { useActivePet } from "../../../src/context/ActivePetContext";
import { useTheme } from "../../../src/context/ThemeContext";
import { getLanguagePreference, setLanguagePreference, type Locale } from "../../../src/i18n";
import { env } from "../../../src/lib/env";
import { PetAvatar } from "../../../src/components/PetAvatar";
import { PetIdentityBadges } from "../../../src/components/PetIdentityBadges";
import { Card } from "../../../src/components/Card";
import { LanguageSwitcher } from "../../../src/components/LanguageSwitcher";
import { formatPetName } from "../../../src/lib/format";
import { getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens";
import type { Pet } from "../../../src/api/types";
@@ -151,53 +150,16 @@ function AppearanceSection() {
);
}
/**
* Local-only preference (mirrors web: locale lives only in the NEXT_LOCALE
* cookie, never on the Owner row — see src/i18n/index.ts), so this needs no
* mutation, just MMKV + i18next's changeLanguage(). Full cache invalidation
* on change is deliberate: pet/post data cached before the switch (species
* and breed names in particular) was fetched under the old x-locale header
* and won't relabel itself otherwise.
*/
/** Section chrome (title + spacing) around the shared LanguageSwitcher — see that component for the actual preference logic (found.md: extracted once the sign-in/sign-up screens needed the same switcher). */
function LanguageSection() {
const { t } = useTranslation();
const { colors } = useTheme();
const styles = useMemo(() => makeStyles(colors), [colors]);
const queryClient = useQueryClient();
const [preference, setPreferenceState] = useState<Locale | null>(() => getLanguagePreference());
const languageOptions = [
{ value: "de" as const, label: t("pets.languageGerman"), flag: "🇩🇪" },
{ value: "en" as const, label: t("pets.languageEnglish"), flag: "🇬🇧" },
{ value: null, label: t("pets.languageAuto"), flag: "🌐" },
];
const handleSelect = (value: Locale | null) => {
setPreferenceState(value);
setLanguagePreference(value);
queryClient.invalidateQueries();
};
return (
<View style={styles.appearanceSection}>
<Text style={styles.appearanceTitle}>{t("pets.languageTitle")}</Text>
<View style={styles.appearanceRow}>
{languageOptions.map((option) => {
const active = preference === option.value;
return (
<Pressable
key={option.label}
style={[styles.appearanceOption, active && styles.appearanceOptionActive]}
onPress={() => handleSelect(option.value)}
>
<Text style={styles.flagEmoji}>{option.flag}</Text>
<Text style={[styles.appearanceOptionText, active && styles.appearanceOptionTextActive]}>
{option.label}
</Text>
</Pressable>
);
})}
</View>
<LanguageSwitcher />
</View>
);
}
@@ -265,7 +227,6 @@ function makeStyles(colors: ColorTokens) {
appearanceOptionActive: { backgroundColor: colors.marigoldOrange, borderColor: colors.marigoldOrange },
appearanceOptionText: { ...typography.body, fontSize: 13, color: colors.ink },
appearanceOptionTextActive: { color: colors.pureWhite },
flagEmoji: { fontSize: 16 },
legalSection: { marginTop: 28, gap: 12, alignItems: "center" },
legalLink: { ...typography.body, fontSize: 13, color: colors.marigoldDeep },
versionText: { ...typography.label, textTransform: "none", letterSpacing: 0, marginTop: 4 },
+2
View File
@@ -5,6 +5,7 @@ import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { Button } from "../../src/components/Button";
import { TextField } from "../../src/components/TextField";
import { LanguageSwitcher } from "../../src/components/LanguageSwitcher";
import { useTheme } from "../../src/context/ThemeContext";
import { getTypography, type ColorTokens, type Typography } from "../../src/theme/tokens";
@@ -93,6 +94,7 @@ export default function SignInScreen() {
return (
<View style={styles.container}>
<LanguageSwitcher />
<Text style={styles.title}>{t("auth.signInTitle")}</Text>
<TextField
autoCapitalize="none"
+5 -3
View File
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { Check } from "lucide-react-native";
import { Button } from "../../src/components/Button";
import { TextField } from "../../src/components/TextField";
import { LanguageSwitcher } from "../../src/components/LanguageSwitcher";
import { useTheme } from "../../src/context/ThemeContext";
import { env } from "../../src/lib/env";
import { getTypography, type ColorTokens, type Typography } from "../../src/theme/tokens";
@@ -115,6 +116,7 @@ export default function SignUpScreen() {
return (
<View style={styles.container}>
<LanguageSwitcher />
<Text style={styles.title}>{t("auth.signUpTitle")}</Text>
<TextField
placeholder={t("auth.firstNamePlaceholder")}
@@ -147,10 +149,10 @@ export default function SignUpScreen() {
</Pressable>
<View style={styles.legalLinksRow}>
<Pressable onPress={() => Linking.openURL(`${env.apiUrl}/nutzungsbedingungen`)}>
<Text style={styles.link}>{t("onboarding.termsLink")}</Text>
<Text style={styles.link}>{t("pets.legalTerms")}</Text>
</Pressable>
<Pressable onPress={() => Linking.openURL(`${env.apiUrl}/datenschutz`)}>
<Text style={styles.link}>{t("onboarding.privacyLink")}</Text>
<Text style={styles.link}>{t("pets.legalPrivacy")}</Text>
</Pressable>
</View>
{error ? <Text style={styles.error}>{error}</Text> : null}
@@ -193,6 +195,6 @@ function makeStyles(colors: ColorTokens) {
},
checkboxChecked: { backgroundColor: colors.marigoldOrange, borderColor: colors.marigoldOrange },
checkboxLabel: { ...typography.body, flex: 1, fontSize: 13, lineHeight: 19 },
legalLinksRow: { flexDirection: "row", justifyContent: "center", gap: 16 },
legalLinksRow: { flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: 16 },
});
}
+6 -3
View File
@@ -6,7 +6,8 @@
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal",
@@ -14,7 +15,8 @@
"EXPO_PUBLIC_API_URL": "https://pawfeed.org",
"EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY": "pk_live_Y2xlcmsucGF3ZmVlZC5vcmck",
"EXPO_PUBLIC_SUPABASE_URL": "https://gqhsovpcfgffdtjxumyy.supabase.co"
}
},
"channel": "preview"
},
"production": {
"android": {
@@ -24,7 +26,8 @@
"EXPO_PUBLIC_API_URL": "https://pawfeed.org",
"EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY": "pk_live_Y2xlcmsucGF3ZmVlZC5vcmck",
"EXPO_PUBLIC_SUPABASE_URL": "https://gqhsovpcfgffdtjxumyy.supabase.co"
}
},
"channel": "production"
}
},
"submit": {
+56
View File
@@ -30,6 +30,7 @@
"expo-splash-screen": "~57.0.8",
"expo-status-bar": "~57.0.1",
"expo-system-ui": "~57.0.3",
"expo-updates": "~57.0.21",
"expo-video": "~57.0.3",
"i18next": "^26.4.2",
"lucide-react-native": "^1.41.0",
@@ -6175,6 +6176,12 @@
"expo": "*"
}
},
"node_modules/expo-eas-client": {
"version": "57.0.3",
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-57.0.3.tgz",
"integrity": "sha512-cyC2AZg85DfAt4Ge7jIRmxmL62dUXob4EnrLZatrl+xJ6AuObqjMYf3hx5JYFfvro4ajkFo25qNREPTdiHCYQQ==",
"license": "MIT"
},
"node_modules/expo-file-system": {
"version": "57.0.6",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.6.tgz",
@@ -6480,6 +6487,12 @@
"react-native": "*"
}
},
"node_modules/expo-structured-headers": {
"version": "57.0.0",
"resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-57.0.0.tgz",
"integrity": "sha512-//t9UNPbJSEysc2x4VKJG/u7Osvv5DYJWsET5bqt/B+qcD1by/JXvSQzX3Q/YAgA96xFPontrz6OAPLbO4JKEA==",
"license": "MIT"
},
"node_modules/expo-symbols": {
"version": "57.0.2",
"resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-57.0.2.tgz",
@@ -6516,6 +6529,43 @@
}
}
},
"node_modules/expo-updates": {
"version": "57.0.21",
"resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-57.0.21.tgz",
"integrity": "sha512-JwTmguNvBvISEz9ALwrT8yDLnYuSP4/6kUz0kXkQuC8izebtTGkRPR37Cv39KPheDDA1F+QPhNyvw4lXM/BPvA==",
"license": "MIT",
"dependencies": {
"@expo/code-signing-certificates": "^0.0.6",
"@expo/plist": "^0.8.1",
"@expo/spawn-async": "^1.8.0",
"arg": "^4.1.0",
"chalk": "^4.1.2",
"debug": "^4.3.4",
"expo-eas-client": "~57.0.3",
"expo-manifests": "~57.0.1",
"expo-structured-headers": "~57.0.0",
"expo-updates-interface": "~57.0.1",
"getenv": "^2.0.0",
"glob": "^13.0.0",
"ignore": "^5.3.1",
"nullthrows": "^1.1.1",
"resolve-from": "^5.0.0"
},
"bin": {
"expo-updates": "bin/cli.js"
},
"peerDependencies": {
"expo": "*",
"expo-dev-client": "*",
"react": "*",
"react-native": "*"
},
"peerDependenciesMeta": {
"expo-dev-client": {
"optional": true
}
}
},
"node_modules/expo-updates-interface": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-57.0.1.tgz",
@@ -6525,6 +6575,12 @@
"expo": "*"
}
},
"node_modules/expo-updates/node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"license": "MIT"
},
"node_modules/expo-video": {
"version": "57.0.3",
"resolved": "https://registry.npmjs.org/expo-video/-/expo-video-57.0.3.tgz",
+1
View File
@@ -25,6 +25,7 @@
"expo-splash-screen": "~57.0.8",
"expo-status-bar": "~57.0.1",
"expo-system-ui": "~57.0.3",
"expo-updates": "~57.0.21",
"expo-video": "~57.0.3",
"i18next": "^26.4.2",
"lucide-react-native": "^1.41.0",
@@ -0,0 +1,76 @@
import { useMemo, useState } from "react";
import { View, Text, Pressable, StyleSheet } from "react-native";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { useTheme } from "../context/ThemeContext";
import { getLanguagePreference, setLanguagePreference, type Locale } from "../i18n";
import { getTypography, type ColorTokens, type Typography } from "../theme/tokens";
/**
* Extracted from pets.tsx's LanguageSection — found.md: the sign-in/sign-up
* screens rendered in whatever language expo-localization auto-detected
* (or defaultLocale as a fallback), with no way to override it before
* logging in, unlike the "Meine Tiere" screen. Same MMKV + i18next
* changeLanguage() logic, no backend field (mirrors web: locale lives only
* in a cookie/local preference, never on the Owner row).
*/
export function LanguageSwitcher() {
const { t } = useTranslation();
const { colors } = useTheme();
const styles = useMemo(() => makeStyles(colors), [colors]);
const queryClient = useQueryClient();
const [preference, setPreferenceState] = useState<Locale | null>(() => getLanguagePreference());
const languageOptions = [
{ value: "de" as const, label: t("pets.languageGerman"), flag: "🇩🇪" },
{ value: "en" as const, label: t("pets.languageEnglish"), flag: "🇬🇧" },
{ value: null, label: t("pets.languageAuto"), flag: "🌐" },
];
const handleSelect = (value: Locale | null) => {
setPreferenceState(value);
setLanguagePreference(value);
queryClient.invalidateQueries();
};
return (
<View style={styles.row}>
{languageOptions.map((option) => {
const active = preference === option.value;
return (
<Pressable
key={option.label}
style={[styles.option, active && styles.optionActive]}
onPress={() => handleSelect(option.value)}
>
<Text style={styles.flagEmoji}>{option.flag}</Text>
<Text style={[styles.optionText, active && styles.optionTextActive]}>{option.label}</Text>
</Pressable>
);
})}
</View>
);
}
function makeStyles(colors: ColorTokens) {
const typography: Typography = getTypography(colors);
return StyleSheet.create({
row: { flexDirection: "row", gap: 8 },
option: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 6,
height: 44,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.hairlineGray,
backgroundColor: colors.pureWhite,
},
optionActive: { backgroundColor: colors.marigoldOrange, borderColor: colors.marigoldOrange },
optionText: { ...typography.body, fontSize: 13, color: colors.ink },
optionTextActive: { color: colors.pureWhite },
flagEmoji: { fontSize: 16 },
});
}