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:
+9
-3
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Generated
+56
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user