feat(profile): useDemographics hook + page-reload re-hydration

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<DemographicsResponse>
  ('/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) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-08 21:32:39 +02:00
parent 53d6e69512
commit c4cfd351c4
2 changed files with 60 additions and 6 deletions

View File

@ -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<Demographics>(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<ScrollView | null>(null);
const demographicsAnchorRef = useRef<View | null>(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(() => {});
},
},
],

View File

@ -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<DemographicsResponse>(
'/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,
};
}