282 lines
8.7 KiB
TypeScript
282 lines
8.7 KiB
TypeScript
/**
|
|
* ProfileProviderWithDevBridge
|
|
*
|
|
* Profile provider that bridges dev user state to profile data in dev mode.
|
|
* In production, fetches real profile data from the backend.
|
|
*
|
|
* Follows the same pattern as AuthProviderWithDevBridge.
|
|
*/
|
|
|
|
import type { ReactNode } from 'react';
|
|
import { useMemo, useCallback, useState } from 'react';
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import type { DevUserContextValue } from '@lilith/ui-dev-tools';
|
|
|
|
import { ProfileContext } from './context';
|
|
import { fetchAllProfiles, saveProfile, getAuthHeaders } from './profile-api';
|
|
import type {
|
|
ProfileContextValue,
|
|
ProfileData,
|
|
ProfileType,
|
|
ProfileUpdateData,
|
|
DevUserToProfilesMapper,
|
|
} from './types';
|
|
|
|
interface ProfileProviderWithDevBridgeProps {
|
|
children: ReactNode;
|
|
/** Base URL for profile backend API */
|
|
profileApiUrl: string;
|
|
/** Access token for authenticated requests (production mode) */
|
|
accessToken?: string | null;
|
|
/** User ID for fetching profiles (production mode) */
|
|
userId?: string | null;
|
|
/** Whether user is authenticated */
|
|
isAuthenticated?: boolean;
|
|
/**
|
|
* Function to map dev user state to mock ProfileData.
|
|
* Required for dev mode to work properly.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const mapDevUserToProfiles: DevUserToProfilesMapper = (devUser) => {
|
|
* return devUser.userTypes
|
|
* .filter(t => t !== 'registered-user')
|
|
* .map(t => ({
|
|
* id: `dev-${t}`,
|
|
* userId: devUser.userId,
|
|
* type: mapTypeToProfileType(t),
|
|
* status: 'active',
|
|
* displayName: devUser.displayName,
|
|
* data: {},
|
|
* completionPercentage: 0,
|
|
* createdAt: new Date().toISOString(),
|
|
* updatedAt: new Date().toISOString(),
|
|
* }));
|
|
* };
|
|
* ```
|
|
*/
|
|
mapDevUserToProfiles: DevUserToProfilesMapper;
|
|
}
|
|
|
|
/**
|
|
* Internal component that tries to use DevUser context.
|
|
*/
|
|
function ProfileProviderWithDevBridgeInner({
|
|
children,
|
|
profileApiUrl,
|
|
accessToken,
|
|
userId,
|
|
isAuthenticated = false,
|
|
mapDevUserToProfiles,
|
|
}: ProfileProviderWithDevBridgeProps) {
|
|
const queryClient = useQueryClient();
|
|
const authHeaders = useMemo(() => getAuthHeaders(accessToken), [accessToken]);
|
|
|
|
// Try to get dev user context
|
|
let devUser: DevUserContextValue | null = null;
|
|
let contextError = false;
|
|
|
|
try {
|
|
// Dynamic require to avoid static analysis issues
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const devTools = require('@lilith/ui-dev-tools');
|
|
devUser = devTools.useDevUser();
|
|
} catch {
|
|
contextError = true;
|
|
}
|
|
|
|
// Determine if we're in dev mode
|
|
const isDevMode = !contextError && devUser?.isDevMode && devUser?.isAuthenticated;
|
|
|
|
// Dev mode: derive profiles from dev user state
|
|
const devProfiles = useMemo<ProfileData[]>(() => {
|
|
if (!isDevMode || !devUser) {
|
|
return [];
|
|
}
|
|
|
|
// Extract persona profile data if an active persona has profiles
|
|
const personaProfiles = devUser.activePersona?.profiles;
|
|
|
|
return mapDevUserToProfiles({
|
|
userId: devUser.userId || 'dev-user-id',
|
|
userTypes: devUser.userTypes,
|
|
primaryType: devUser.primaryType,
|
|
displayName: devUser.displayName,
|
|
personaProfiles: personaProfiles as import('./types').DevPersonaProfile[] | undefined,
|
|
});
|
|
}, [isDevMode, devUser, mapDevUserToProfiles]);
|
|
|
|
// Track dev profile updates locally (not persisted)
|
|
const [devProfileUpdates, setDevProfileUpdates] = useState<Record<ProfileType, Partial<ProfileData>>>({} as Record<ProfileType, Partial<ProfileData>>);
|
|
|
|
// Merge dev profiles with local updates
|
|
const mergedDevProfiles = useMemo<ProfileData[]>(() => {
|
|
return devProfiles.map((p) => ({
|
|
...p,
|
|
...devProfileUpdates[p.type],
|
|
}));
|
|
}, [devProfiles, devProfileUpdates]);
|
|
|
|
// Production mode: fetch from backend
|
|
const {
|
|
data: prodProfiles = [],
|
|
isLoading: prodLoading,
|
|
error: prodError,
|
|
refetch: prodRefetch,
|
|
} = useQuery({
|
|
queryKey: ['profiles', userId],
|
|
queryFn: () => fetchAllProfiles(profileApiUrl, authHeaders),
|
|
enabled: !isDevMode && isAuthenticated && !!userId,
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
|
|
// Production mode: update mutation
|
|
const updateMutation = useMutation({
|
|
mutationFn: async ({ type, data }: { type: ProfileType; data: ProfileUpdateData }) => {
|
|
return saveProfile(profileApiUrl, type, data, authHeaders);
|
|
},
|
|
onSuccess: (updatedProfile) => {
|
|
queryClient.setQueryData<ProfileData[]>(['profiles', userId], (old = []) => {
|
|
const index = old.findIndex((p) => p.type === updatedProfile.type);
|
|
if (index >= 0) {
|
|
const updated = [...old];
|
|
updated[index] = updatedProfile;
|
|
return updated;
|
|
}
|
|
return [...old, updatedProfile];
|
|
});
|
|
},
|
|
});
|
|
|
|
// Select profiles based on mode
|
|
const profiles = isDevMode ? mergedDevProfiles : prodProfiles;
|
|
const isLoading = isDevMode ? false : prodLoading;
|
|
const error = isDevMode ? null : (prodError as Error | null);
|
|
|
|
// Derive primary profile
|
|
const primaryProfile = useMemo(() => {
|
|
if (isDevMode && devUser?.primaryType) {
|
|
// In dev mode, use the primary type from dev user
|
|
const primaryTypeMap: Record<string, ProfileType> = {
|
|
'registered-provider': 'provider',
|
|
'registered-client': 'client',
|
|
'registered-investor': 'investor',
|
|
};
|
|
const mappedType = primaryTypeMap[devUser.primaryType];
|
|
if (mappedType) {
|
|
return profiles.find((p) => p.type === mappedType) || null;
|
|
}
|
|
}
|
|
return profiles.find((p) => p.isPrimary) || profiles[0] || null;
|
|
}, [isDevMode, devUser?.primaryType, profiles]);
|
|
|
|
// Get profile by type helper
|
|
const getProfileByType = useCallback(
|
|
(type: ProfileType): ProfileData | null => {
|
|
return profiles.find((p) => p.type === type) || null;
|
|
},
|
|
[profiles]
|
|
);
|
|
|
|
// Update profile - dev mode stores locally, prod mode calls API
|
|
const updateProfile = useCallback(
|
|
async (type: ProfileType, data: ProfileUpdateData): Promise<ProfileData> => {
|
|
if (isDevMode) {
|
|
// Dev mode: update locally
|
|
const existing = profiles.find((p) => p.type === type);
|
|
const updated: ProfileData = {
|
|
id: existing?.id || `dev-${type}`,
|
|
userId: devUser?.userId || 'dev-user-id',
|
|
type,
|
|
status: existing?.status || 'active',
|
|
displayName: data.displayName ?? existing?.displayName,
|
|
bio: data.bio ?? existing?.bio,
|
|
avatarUrl: data.avatarUrl ?? existing?.avatarUrl,
|
|
data: { ...existing?.data, ...data.data },
|
|
completionPercentage: existing?.completionPercentage || 0,
|
|
createdAt: existing?.createdAt || new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
setDevProfileUpdates((prev) => ({
|
|
...prev,
|
|
[type]: updated,
|
|
}));
|
|
|
|
return updated;
|
|
}
|
|
|
|
// Production mode: call API
|
|
return updateMutation.mutateAsync({ type, data });
|
|
},
|
|
[isDevMode, profiles, devUser?.userId, updateMutation]
|
|
);
|
|
|
|
// Refetch helper
|
|
const refetchProfiles = useCallback(async () => {
|
|
if (!isDevMode) {
|
|
await prodRefetch();
|
|
}
|
|
// Dev mode: no-op (profiles are derived from dev user state)
|
|
}, [isDevMode, prodRefetch]);
|
|
|
|
const contextValue: ProfileContextValue = useMemo(
|
|
() => ({
|
|
profiles,
|
|
primaryProfile,
|
|
isLoading,
|
|
error,
|
|
refetch: refetchProfiles,
|
|
updateProfile,
|
|
isUpdating: updateMutation.isPending,
|
|
getProfileByType,
|
|
isDevMode: !!isDevMode,
|
|
}),
|
|
[
|
|
profiles,
|
|
primaryProfile,
|
|
isLoading,
|
|
error,
|
|
refetchProfiles,
|
|
updateProfile,
|
|
updateMutation.isPending,
|
|
getProfileByType,
|
|
isDevMode,
|
|
]
|
|
);
|
|
|
|
return (
|
|
<ProfileContext.Provider value={contextValue}>
|
|
{children}
|
|
</ProfileContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Profile provider with dev mode bridge.
|
|
*
|
|
* When dev auth is active (via DevUserProvider), this component
|
|
* derives mock profile data from the dev user state. In production,
|
|
* it fetches real profiles from the backend.
|
|
*
|
|
* NOTE: Must be used within a QueryClientProvider and ideally
|
|
* within AuthProviderWithDevBridge for consistent auth/profile state.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <DevUserProvider userTypes={DEV_USER_TYPES} storageKey="myapp_dev_user">
|
|
* <AuthProviderWithDevBridge ssoUrl={ssoUrl} mapDevUser={mapDevUser}>
|
|
* <ProfileProviderWithDevBridge
|
|
* profileApiUrl="https://profile.api.atlilith.com"
|
|
* mapDevUserToProfiles={mapDevUserToProfiles}
|
|
* >
|
|
* <App />
|
|
* </ProfileProviderWithDevBridge>
|
|
* </AuthProviderWithDevBridge>
|
|
* </DevUserProvider>
|
|
* ```
|
|
*/
|
|
export function ProfileProviderWithDevBridge(props: ProfileProviderWithDevBridgeProps) {
|
|
return <ProfileProviderWithDevBridgeInner {...props} />;
|
|
}
|