From c4cfd351c4ffd1662471f7e0837e4ad52278f957 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Fri, 8 May 2026 21:32:39 +0200 Subject: [PATCH] feat(profile): useDemographics hook + page-reload re-hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-Bug: Demographics werden korrekt gespeichert (DB verified), aber nach Page-Reload sah User leere Felder → dachte save kaputt. Root: kein GET-endpoint + kein server-state-rehydrate nach PATCH. - hooks/useProfileData.ts: useDemographics() wraps useFetchOnce ('/api/profile/me/demographics'), splittet in fields + meta (consentAt/withdrawnAt) - app/profile/index.tsx: serverDemographics ?? EMPTY_DEMOGRAPHICS const statt local state. Nach PATCH/DELETE: reloadDemographics() pulled fresh server data. Edge-cases: - 404 (endpoint nicht live) → fallback EMPTY, kein crash - loading → EMPTY initial bis fetch resolved, konsistent mit other hooks - withdrawnAt set → demoComplete=false (Demographics-Hint sichtbar trotz potentiell noch befüllter felder durch race-condition) Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/rebreak-native/app/profile/index.tsx | 20 ++++++--- apps/rebreak-native/hooks/useProfileData.ts | 46 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/apps/rebreak-native/app/profile/index.tsx b/apps/rebreak-native/app/profile/index.tsx index 18d3657..57b33d6 100644 --- a/apps/rebreak-native/app/profile/index.tsx +++ b/apps/rebreak-native/app/profile/index.tsx @@ -18,6 +18,7 @@ import { useApprovedDomains, useCooldownHistory, useSosInsights, + useDemographics, } from '../../hooks/useProfileData'; import { apiFetch } from '../../lib/api'; @@ -84,7 +85,6 @@ function mapHelpedBy(helpedBy: { export default function ProfileScreen() { const insets = useSafeAreaInsets(); const [bannerDismissed, setBannerDismissed] = useState(false); - const [demographics, setDemographics] = useState(EMPTY_DEMOGRAPHICS); const [demographicsExpanded, setDemographicsExpanded] = useState(false); const { me } = useMe(); const { user } = useAuthStore(); @@ -93,6 +93,13 @@ export default function ProfileScreen() { const { domains: approvedDomainsData } = useApprovedDomains(); const { cooldownHistory } = useCooldownHistory(); const { sosInsights } = useSosInsights(); + const { + demographics: serverDemographics, + withdrawnAt, + reload: reloadDemographics, + } = useDemographics(); + + const demographics: Demographics = serverDemographics ?? EMPTY_DEMOGRAPHICS; const scrollViewRef = useRef(null); const demographicsAnchorRef = useRef(null); @@ -117,7 +124,7 @@ export default function ProfileScreen() { const streakStartDate = formatStreakStartDate(me?.created_at); const showDigaBanner = currentStreak >= 30 && !bannerDismissed; - const demoComplete = isDemographicsComplete(demographics); + const demoComplete = !withdrawnAt && isDemographicsComplete(demographics); function scrollToDemographics() { const node = demographicsAnchorRef.current; @@ -230,12 +237,12 @@ export default function ProfileScreen() { plan={profile.plan} expanded={demographicsExpanded} onChange={async (next) => { - setDemographics(next); try { const result = await apiFetch<{ trialAwarded: boolean; expiresAt: string | null }>( '/api/profile/me/demographics', { method: 'PATCH', body: next }, ); + reloadDemographics(); if (result.trialAwarded) { Alert.alert( 'Pro-Woche freigeschaltet', @@ -243,7 +250,7 @@ export default function ProfileScreen() { ); } } catch { - // write failed — local state still updated optimistically + // write failed — optimistic update not applied, server state preserved } }} onRevokeConsent={() => { @@ -256,8 +263,9 @@ export default function ProfileScreen() { text: 'Loschen', style: 'destructive', onPress: () => { - apiFetch('/api/profile/me/demographics', { method: 'DELETE' }).catch(() => {}); - setDemographics(EMPTY_DEMOGRAPHICS); + apiFetch('/api/profile/me/demographics', { method: 'DELETE' }) + .then(() => reloadDemographics()) + .catch(() => {}); }, }, ], diff --git a/apps/rebreak-native/hooks/useProfileData.ts b/apps/rebreak-native/hooks/useProfileData.ts index b827d67..aa9e9bc 100644 --- a/apps/rebreak-native/hooks/useProfileData.ts +++ b/apps/rebreak-native/hooks/useProfileData.ts @@ -141,3 +141,49 @@ export function useSosInsights() { return { sosInsights: data, loading, error, reload }; } +export type Demographics = { + birthYear: number | null; + gender: string | null; + maritalStatus: string | null; + employmentStatus: string | null; + shiftWork: boolean | null; + industry: string | null; + jobTenure: string | null; + bundesland: string | null; + city: string | null; +}; + +type DemographicsResponse = Demographics & { + consentAt: string | null; + withdrawnAt: string | null; +}; + +export function useDemographics() { + const { data, loading, error, reload } = useFetchOnce( + '/api/profile/me/demographics', + ); + + const demographics: Demographics | null = data + ? { + birthYear: data.birthYear, + gender: data.gender, + maritalStatus: data.maritalStatus, + employmentStatus: data.employmentStatus, + shiftWork: data.shiftWork, + industry: data.industry, + jobTenure: data.jobTenure, + bundesland: data.bundesland, + city: data.city, + } + : null; + + return { + demographics, + consentAt: data?.consentAt ?? null, + withdrawnAt: data?.withdrawnAt ?? null, + loading, + error, + reload, + }; +} +