Stage 1+2 des post-signup Onboarding-Flows: - Welcome-Screen: dark-slate Full-Screen mit Pulse-Hero, 3 Mission-Bullets, DSGVO-Box, CTA "Los geht's" - Nickname-Spotlight via react-native-copilot ums TextInput in /profile/edit, auto-start wenn step='nickname', nach Save → step='block' + back to /(app) - Backend: Profile.onboardingStep enum (welcome/nickname/block/done), Migration mit Backfill (existing → done), PATCH /api/profile/me/onboarding-step, /api/auth/me erweitert - Frontend: CopilotProvider in root, Routing-Gate in (app)/_layout, useMe um onboardingStep ergänzt - i18n (de/en/fr) für onboarding.welcome.* + onboarding.nickname_spotlight.* Stage 3 (Block-Aktivierung-Spotlight) folgt in nächster Session — der bestehende ProtectionOnboardingSheet auf Android wird daran angebunden. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { apiFetch } from '../lib/api';
|
|
|
|
export type Plan = 'free' | 'pro' | 'legend';
|
|
|
|
/**
|
|
* Single source of truth für den eingeloggten User. /api/auth/me joint
|
|
* `auth.users` mit `rebreak.profiles` server-side — wir bekommen alles in
|
|
* einem Request: plan, avatar, nickname, streak.
|
|
*
|
|
* Live-Update-Pattern (siehe RECOVERY_LOG): nach Profile-Edit (PATCH /api/auth/me)
|
|
* MUSS `invalidateMe()` aufgerufen werden — alle useMe-Konsumenten (AppHeader,
|
|
* PostCard, ComposeCard, etc.) re-fetchen automatisch via Listener-Subscribe.
|
|
*
|
|
* WICHTIG: nicht aus `supabase.auth.getUser().user_metadata` lesen — das
|
|
* sind nur die JWT-Claims vom Signup-Zeitpunkt, NICHT der aktuelle Profile-
|
|
* Stand (Avatar/Nickname/Plan werden via Profile-Edit-API geupdated, landen
|
|
* in der DB, NICHT zurück ins JWT-Claim).
|
|
*/
|
|
export type OnboardingStep = 'welcome' | 'nickname' | 'block' | 'done';
|
|
|
|
export type Me = {
|
|
id: string;
|
|
email: string;
|
|
username: string;
|
|
nickname: string | null;
|
|
avatar: string | null;
|
|
plan: Plan;
|
|
streak: number;
|
|
lyraVoiceId: string | null;
|
|
onboardingStep: OnboardingStep;
|
|
created_at?: string;
|
|
};
|
|
|
|
let cachedMe: Me | null = null;
|
|
const listeners = new Set<() => void>();
|
|
|
|
/**
|
|
* Lädt /api/auth/me neu und benachrichtigt ALLE useMe-Konsumenten in der App.
|
|
* Nach jedem PATCH /api/auth/me aufrufen — sonst sehen Konsumenten alten Cache.
|
|
*/
|
|
export function invalidateMe(): void {
|
|
cachedMe = null;
|
|
for (const cb of listeners) cb();
|
|
}
|
|
|
|
export function useMe(): { me: Me | null; loading: boolean; reload: () => void } {
|
|
const [me, setMe] = useState<Me | null>(cachedMe);
|
|
const [loading, setLoading] = useState(cachedMe === null);
|
|
const [version, setVersion] = useState(0);
|
|
|
|
// Auf globale Invalidierung lauschen (Avatar-/Nickname-Update aus Profile-Edit)
|
|
useEffect(() => {
|
|
const cb = () => setVersion((v) => v + 1);
|
|
listeners.add(cb);
|
|
return () => {
|
|
listeners.delete(cb);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
// Falls cache schon frisch ist (von anderem Konsumenten gerade geladen): nutzen
|
|
if (cachedMe !== null) {
|
|
setMe(cachedMe);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
try {
|
|
const res = await apiFetch<Me>('/api/auth/me');
|
|
if (cancelled) return;
|
|
cachedMe = res;
|
|
setMe(res);
|
|
} catch (e) {
|
|
console.warn('[useMe] fetch failed:', e);
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [version]);
|
|
|
|
return {
|
|
me,
|
|
loading,
|
|
reload: () => {
|
|
invalidateMe();
|
|
},
|
|
};
|
|
}
|