diff --git a/mobile/app.json b/mobile/app.json index 705ac04..4f1bcbd 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -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" + } } } diff --git a/mobile/app/(app)/(tabs)/pets.tsx b/mobile/app/(app)/(tabs)/pets.tsx index 8b43286..bcb4258 100644 --- a/mobile/app/(app)/(tabs)/pets.tsx +++ b/mobile/app/(app)/(tabs)/pets.tsx @@ -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(() => 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 ( {t("pets.languageTitle")} - - {languageOptions.map((option) => { - const active = preference === option.value; - return ( - handleSelect(option.value)} - > - {option.flag} - - {option.label} - - - ); - })} - + ); } @@ -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 }, diff --git a/mobile/app/(auth)/sign-in.tsx b/mobile/app/(auth)/sign-in.tsx index 5ffd53e..5a99e73 100644 --- a/mobile/app/(auth)/sign-in.tsx +++ b/mobile/app/(auth)/sign-in.tsx @@ -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 ( + {t("auth.signInTitle")} + {t("auth.signUpTitle")} Linking.openURL(`${env.apiUrl}/nutzungsbedingungen`)}> - {t("onboarding.termsLink")} + {t("pets.legalTerms")} Linking.openURL(`${env.apiUrl}/datenschutz`)}> - {t("onboarding.privacyLink")} + {t("pets.legalPrivacy")} {error ? {error} : 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 }, }); } diff --git a/mobile/eas.json b/mobile/eas.json index d0159fe..5d8b1e4 100644 --- a/mobile/eas.json +++ b/mobile/eas.json @@ -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": { diff --git a/mobile/package-lock.json b/mobile/package-lock.json index b34cf39..e686ddb 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -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", diff --git a/mobile/package.json b/mobile/package.json index 129d1ec..484d3d0 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -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", diff --git a/mobile/src/components/LanguageSwitcher.tsx b/mobile/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..cf9e1da --- /dev/null +++ b/mobile/src/components/LanguageSwitcher.tsx @@ -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(() => 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 ( + + {languageOptions.map((option) => { + const active = preference === option.value; + return ( + handleSelect(option.value)} + > + {option.flag} + {option.label} + + ); + })} + + ); +} + +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 }, + }); +}