/** * Hook for fetching user profile from Zitadel OIDC userinfo endpoint. * * GDPR Compliance: User profile data is never stored in our database. * This hook fetches it directly from Zitadel when needed for display. */ import { useState, useEffect, useCallback } from "react"; import * as SecureStore from "expo-secure-store"; interface UserProfile { sub: string; name?: string; preferredUsername?: string; email?: string; emailVerified?: boolean; picture?: string; } const ACCESS_TOKEN_KEY = "zitadel_access_token"; /** * Hook to fetch and cache user profile from Zitadel userinfo endpoint. * Profile is held in memory only - never persisted to our database. */ export function useUserProfile() { const [profile, setProfile] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const fetchProfile = useCallback(async () => { setIsLoading(true); setError(null); try { // Get stored access token const accessToken = await SecureStore.getItemAsync(ACCESS_TOKEN_KEY); if (!accessToken) { setProfile(null); return null; } const issuer = process.env.EXPO_PUBLIC_ZITADEL_ISSUER; if (!issuer) { throw new Error("ZITADEL_ISSUER not configured"); } // Fetch from OIDC userinfo endpoint const response = await fetch(`${issuer}/oidc/v1/userinfo`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { if (response.status === 401) { // Token expired - clear it await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY); setProfile(null); return null; } throw new Error(`Failed to fetch user profile: ${response.status}`); } const data = await response.json(); const userProfile: UserProfile = { sub: data.sub, name: data.name, preferredUsername: data.preferred_username, email: data.email, emailVerified: data.email_verified, picture: data.picture, }; setProfile(userProfile); return userProfile; } catch (err) { const message = err instanceof Error ? err.message : "Failed to fetch profile"; setError(message); console.error("[useUserProfile]", message); return null; } finally { setIsLoading(false); } }, []); const clearProfile = useCallback(() => { setProfile(null); setError(null); }, []); return { profile, isLoading, error, fetchProfile, clearProfile, }; } /** * Store access token after successful auth (called from useAuth). */ export async function storeAccessToken(token: string): Promise { await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, token); } /** * Clear access token on sign out. */ export async function clearAccessToken(): Promise { await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY); }