diff --git a/mobile/src/components/auth/Reveal.tsx b/mobile/src/components/Reveal.tsx
similarity index 100%
rename from mobile/src/components/auth/Reveal.tsx
rename to mobile/src/components/Reveal.tsx
diff --git a/mobile/src/components/onboarding/OnboardingGuidelines.tsx b/mobile/src/components/onboarding/OnboardingGuidelines.tsx
deleted file mode 100644
index 1bda3c4..0000000
--- a/mobile/src/components/onboarding/OnboardingGuidelines.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { View, Text, Pressable, ActivityIndicator, Linking, StyleSheet } from "react-native";
-import { useEffect, useMemo } from "react";
-import { useTranslation } from "react-i18next";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { ScrollText } from "lucide-react-native";
-import { useTRPC } from "../../api/trpc";
-import { env } from "../../lib/env";
-import { Button } from "../Button";
-import { radii, getTypography, type ColorTokens, type Typography } from "../../theme/tokens";
-import { useTheme } from "../../context/ThemeContext";
-
-type Props = { onContinue: () => void };
-
-/**
- * Port of web's src/components/legal/OnboardingGuidelines.tsx. Auto-skips
- * via onContinue() once acknowledged, or if nothing has ever been published
- * (legal.getPendingUpdate returns null) — matches web's admin-editable
- * LegalDocumentVersion content, not a hardcoded first-run screen.
- */
-export function OnboardingGuidelines({ onContinue }: Props) {
- const { t } = useTranslation();
- const { colors } = useTheme();
- const styles = useMemo(() => makeStyles(colors), [colors]);
- const trpc = useTRPC();
- const queryClient = useQueryClient();
-
- const { data: pending, isLoading } = useQuery(trpc.legal.getPendingUpdate.queryOptions());
-
- const acknowledgeMutation = useMutation(
- trpc.legal.acknowledge.mutationOptions({
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: trpc.legal.getPendingUpdate.queryOptions().queryKey });
- onContinue();
- },
- })
- );
-
- const hasNothingToAcknowledge = !isLoading && (!pending || pending.length === 0);
-
- useEffect(() => {
- if (hasNothingToAcknowledge) onContinue();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [hasNothingToAcknowledge]);
-
- if (isLoading || hasNothingToAcknowledge) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
- {t("onboarding.guidelinesTitle")}
-
- {t("onboarding.guidelinesSubtitle")}
-
-
- {pending!.map((version) => (
-
- {version.changeSummary}
-
- ))}
-
-
- Linking.openURL(`${env.apiUrl}/nutzungsbedingungen`)}>
- {t("onboarding.termsLink")}
-
- Linking.openURL(`${env.apiUrl}/datenschutz`)}>
- {t("onboarding.privacyLink")}
-
-
-
- );
-}
-
-function makeStyles(colors: ColorTokens) {
- const typography: Typography = getTypography(colors);
- return StyleSheet.create({
- center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 40 },
- container: { padding: 24, gap: 4 },
- titleRow: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 4 },
- title: { ...typography.title, fontSize: 18 },
- subtitle: { ...typography.body, color: colors.quietGray, marginBottom: 16 },
- card: {
- gap: 10,
- padding: 14,
- borderRadius: radii.xl,
- borderWidth: 1,
- borderColor: colors.hairlineGray,
- backgroundColor: colors.pureWhite,
- marginBottom: 16,
- },
- summary: { ...typography.body },
- link: { ...typography.caption, color: colors.marigoldDeep, marginBottom: 8 },
- cta: { marginTop: 12 },
- });
-}
diff --git a/mobile/src/components/onboarding/PetCreateForm.tsx b/mobile/src/components/onboarding/PetCreateForm.tsx
deleted file mode 100644
index b3f3252..0000000
--- a/mobile/src/components/onboarding/PetCreateForm.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import { useMemo, useState } from "react";
-import { View, Text, StyleSheet } from "react-native";
-import { useTranslation } from "react-i18next";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { useTRPC } from "../../api/trpc";
-import { TextField } from "../TextField";
-import { Button } from "../Button";
-import { OptionPicker, type PickerOption } from "../OptionPicker";
-import { BreedPicker } from "../pet/BreedPicker";
-import { useTheme } from "../../context/ThemeContext";
-import { getTypography, type ColorTokens } from "../../theme/tokens";
-
-type Props = { onSuccess: (pet: { id: string; name: string }) => void };
-
-/**
- * Port of web's src/components/pet/PetForm.tsx in create mode — name,
- * nickname, species/breed (data-driven, never hardcoded per CLAUDE.md),
- * bio, adoption story. Avatar upload happens in a separate step after the
- * pet exists (see onboarding/pet.tsx), matching web's two-phase flow.
- */
-export function PetCreateForm({ onSuccess }: Props) {
- const { t } = useTranslation();
- const trpc = useTRPC();
- const queryClient = useQueryClient();
- const { colors } = useTheme();
- const styles = useMemo(() => makeStyles(colors), [colors]);
- const [name, setName] = useState("");
- const [nickname, setNickname] = useState("");
- const [speciesId, setSpeciesId] = useState(null);
- const [speciesName, setSpeciesName] = useState(null);
- const [breedId, setBreedId] = useState(null);
- const [breedName, setBreedName] = useState(null);
- const [bio, setBio] = useState("");
- const [adoptionStory, setAdoptionStory] = useState("");
- const [error, setError] = useState(null);
-
- const createMutation = useMutation(
- trpc.pets.create.mutationOptions({
- onSuccess: (pet) => {
- // Ohne dieses Invalidate bleibt ActivePetContext.pets (trpc.pets.list)
- // auf dem alten "keine Tiere"-Stand stehen, bis die App neu startet —
- // OnboardingRedirect wirft dann sofort wieder auf /onboarding/pet
- // zurück, sobald man nach dem Avatar-Schritt in (app) landet.
- queryClient.invalidateQueries({ queryKey: trpc.pets.list.queryOptions().queryKey });
- onSuccess({ id: pet.id, name: pet.name });
- },
- onError: (err) => setError(err.message),
- })
- );
-
- const canSubmit = name.trim().length > 0 && !!speciesId && !createMutation.isPending;
-
- return (
-
- {t("petForm.createTitle")}
- {t("petForm.createSubtitle")}
-
- {t("petForm.name")}
-
-
- {t("petForm.nickname")}
-
-
- {t("petForm.species")}
- {
- setSpeciesId(option.id);
- setSpeciesName(option.name);
- setBreedId(null);
- setBreedName(null);
- }}
- />
-
- {t("petForm.breed")}
- { setBreedId(o.id); setBreedName(o.name); }} />
-
- {t("petForm.bio")}
-
-
- {t("petForm.adoptionStory")}
-
-
- {error ? {error} : null}
-
-
- );
-}
-
-function SpeciesPicker({ selectedName, onSelect }: { selectedName: string | null; onSelect: (o: PickerOption) => void }) {
- const { t, i18n } = useTranslation();
- const trpc = useTRPC();
- const [open, setOpen] = useState(false);
- const { data: species = [], isLoading } = useQuery(trpc.pets.listSpecies.queryOptions({ locale: i18n.language }));
-
- return (
- setOpen((v) => !v)}
- selectedName={selectedName}
- placeholder={t("petForm.selectSpecies")}
- isLoading={isLoading}
- options={species}
- onSelect={(o) => {
- onSelect(o);
- setOpen(false);
- }}
- />
- );
-}
-
-function makeStyles(colors: ColorTokens) {
- const typography = getTypography(colors);
- return StyleSheet.create({
- container: { padding: 24, gap: 6 },
- title: { ...typography.title, fontSize: 18, marginBottom: 4 },
- subtitle: { ...typography.body, color: colors.quietGray, marginBottom: 16 },
- label: { ...typography.caption, marginTop: 10 },
- multiline: { height: 80, textAlignVertical: "top", paddingTop: 12 },
- error: { ...typography.body, color: colors.alertRed, marginTop: 8 },
- cta: { marginTop: 16 },
- });
-}
diff --git a/mobile/src/components/onboarding/UploadConsent.tsx b/mobile/src/components/onboarding/UploadConsent.tsx
deleted file mode 100644
index 8a2b2cf..0000000
--- a/mobile/src/components/onboarding/UploadConsent.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { useEffect, useMemo, useState } from "react";
-import { View, Text, Pressable, ActivityIndicator, Linking, StyleSheet } from "react-native";
-import { useTranslation } from "react-i18next";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { ShieldCheck, Check } from "lucide-react-native";
-import { useTRPC } from "../../api/trpc";
-import { env } from "../../lib/env";
-import { Button } from "../Button";
-import { radii, getTypography, type ColorTokens, type Typography } from "../../theme/tokens";
-import { useTheme } from "../../context/ThemeContext";
-
-type Props = { onContinue: () => void };
-
-/**
- * Port of web's src/components/legal/UploadConsent.tsx — two mandatory,
- * blocking Willenserklärungen (content rights + data-processing consent,
- * Gitea #3) shown once before the very first pet can be created. Skips
- * automatically if already consented (legal.getUploadConsentStatus).
- */
-export function UploadConsent({ onContinue }: Props) {
- const { t } = useTranslation();
- const { colors } = useTheme();
- const styles = useMemo(() => makeStyles(colors), [colors]);
- const trpc = useTRPC();
- const queryClient = useQueryClient();
- const [contentRightsChecked, setContentRightsChecked] = useState(false);
- const [dataProcessingChecked, setDataProcessingChecked] = useState(false);
-
- const { data: status, isLoading } = useQuery(trpc.legal.getUploadConsentStatus.queryOptions());
-
- const acknowledgeMutation = useMutation(
- trpc.legal.acknowledgeUploadConsent.mutationOptions({
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: trpc.legal.getUploadConsentStatus.queryOptions().queryKey });
- onContinue();
- },
- })
- );
-
- const alreadyConsented = !isLoading && status?.hasConsented === true;
-
- useEffect(() => {
- if (alreadyConsented) onContinue();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [alreadyConsented]);
-
- if (isLoading || alreadyConsented) {
- return (
-
-
-
- );
- }
-
- const canContinue = contentRightsChecked && dataProcessingChecked;
-
- return (
-
-
-
- {t("onboarding.consentTitle")}
-
- {t("onboarding.consentSubtitle")}
-
- setContentRightsChecked((v) => !v)}
- label={t("onboarding.contentRightsLabel")}
- />
- setDataProcessingChecked((v) => !v)}
- label={t("onboarding.dataProcessingLabel")}
- />
-
- Linking.openURL(`${env.apiUrl}/datenschutz`)}>
- {t("onboarding.consentPrivacyLink")}
-
-
-
- );
-}
-
-function ConsentCheckbox({ checked, onToggle, label }: { checked: boolean; onToggle: () => void; label: string }) {
- const { colors } = useTheme();
- const styles = useMemo(() => makeStyles(colors), [colors]);
- return (
-
-
- {checked && }
-
- {label}
-
- );
-}
-
-function makeStyles(colors: ColorTokens) {
- const typography: Typography = getTypography(colors);
- return StyleSheet.create({
- center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 40 },
- container: { padding: 24, gap: 4 },
- titleRow: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 4 },
- title: { ...typography.title, fontSize: 18 },
- subtitle: { ...typography.body, color: colors.quietGray, marginBottom: 16 },
- checkboxRow: {
- flexDirection: "row",
- gap: 12,
- padding: 14,
- borderRadius: radii.xl,
- borderWidth: 1,
- borderColor: colors.hairlineGray,
- marginBottom: 12,
- },
- checkbox: {
- width: 20,
- height: 20,
- borderRadius: 5,
- borderWidth: 1.5,
- borderColor: colors.hairlineGray,
- alignItems: "center",
- justifyContent: "center",
- marginTop: 2,
- },
- checkboxChecked: { backgroundColor: colors.marigoldOrange, borderColor: colors.marigoldOrange },
- checkboxLabel: { ...typography.body, flex: 1, fontSize: 13, lineHeight: 19 },
- link: { ...typography.caption, color: colors.marigoldDeep, marginBottom: 16, marginTop: 4 },
- cta: { marginTop: 4 },
- });
-}
diff --git a/mobile/src/components/settings/InviteFriendsSection.tsx b/mobile/src/components/referrals/InviteCard.tsx
similarity index 100%
rename from mobile/src/components/settings/InviteFriendsSection.tsx
rename to mobile/src/components/referrals/InviteCard.tsx