From 45b808f51c402e655bc06c5a3f30021b29d19461 Mon Sep 17 00:00:00 2001 From: janithjay Date: Tue, 18 Aug 2026 00:12:11 +0530 Subject: [PATCH] Populate Next.js UserProfile fields from users/me/meta schema Signed-off-by: janithjay --- packages/nextjs/src/ThunderIDNextClient.ts | 23 ++++++++++++++ .../presentation/UserProfile/UserProfile.tsx | 3 +- .../contexts/ThunderID/ThunderIDProvider.tsx | 18 +++++++++-- .../nextjs/src/server/ThunderIDProvider.tsx | 10 +++++++ .../src/server/actions/getUserSchemaAction.ts | 30 +++++++++++++++++++ 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 packages/nextjs/src/server/actions/getUserSchemaAction.ts diff --git a/packages/nextjs/src/ThunderIDNextClient.ts b/packages/nextjs/src/ThunderIDNextClient.ts index f6dd8095..b3759bc5 100644 --- a/packages/nextjs/src/ThunderIDNextClient.ts +++ b/packages/nextjs/src/ThunderIDNextClient.ts @@ -4,6 +4,7 @@ import { ThunderIDNodeClient, ThunderIDRuntimeError, + AttributeSchema, AuthClientConfig, EmbeddedSignInFlowResponse, ExtendedAuthorizeRequestUrlParams, @@ -19,6 +20,7 @@ import { extractUserClaimsFromIdToken, generateFlattenedUserProfile, getUsersMe, + getUsersMeMeta, updateMeProfile, resolveResourceEndpoint, } from '@thunderid/node'; @@ -154,6 +156,27 @@ class ThunderIDNextClient e } } + async getUserSchema(userId?: string): Promise | null> { + await this.ensureInitialized(); + + try { + const configData: AuthClientConfig = await this.getStorageManager().getConfigData(); + const baseUrl: string | undefined = configData?.baseUrl; + + const {schema} = await getUsersMeMeta({ + baseUrl, + url: resolveResourceEndpoint('usersMeMeta', configData), + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + }); + + return schema ?? null; + } catch (error) { + return null; + } + } + override async updateUserProfile(payload: any, userId?: string): Promise { await this.ensureInitialized(); diff --git a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx index 0274f500..e7ad8543 100644 --- a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx @@ -40,7 +40,7 @@ export type UserProfileProps = Omit = ({preferences, editable = true, ...rest}: UserProfileProps): ReactElement => { const {preferences: contextPreferences} = useThunderID(); - const {profile, flattenedProfile, onUpdateProfile, updateProfile} = useUser(); + const {profile, flattenedProfile, onUpdateProfile, updateProfile, userSchema} = useUser(); const resolvedPreferences = { ...contextPreferences, ...preferences, @@ -81,6 +81,7 @@ const UserProfile: FC = ({preferences, editable = true, ...res Promise<{data: {user: User}; error: string; success: boolean}>; user: User | null; userProfile: UserProfile; + userSchema?: Record | null; }; const ThunderIDClientProvider: FC> = ({ @@ -86,6 +88,7 @@ const ThunderIDClientProvider: FC(true); const [user, setUser] = useState(_user); const [userProfile, setUserProfile] = useState(_userProfile); + const [userSchema, setUserSchema] = useState | null>(_userSchema); useEffect(() => { setUserProfile(_userProfile); }, [_userProfile]); + useEffect(() => { + setUserSchema(_userSchema); + }, [_userSchema]); + useEffect(() => { setUser(_user); }, [_user]); @@ -368,7 +376,7 @@ const ThunderIDClientProvider: FC - + {children} diff --git a/packages/nextjs/src/server/ThunderIDProvider.tsx b/packages/nextjs/src/server/ThunderIDProvider.tsx index db88f18b..84ca163d 100644 --- a/packages/nextjs/src/server/ThunderIDProvider.tsx +++ b/packages/nextjs/src/server/ThunderIDProvider.tsx @@ -5,6 +5,7 @@ import { ThunderIDRuntimeError, + AttributeSchema, FlowMetadataResponse, FlowMetaType, IdToken, @@ -21,6 +22,7 @@ import getSessionId from './actions/getSessionId'; import getSessionPayload from './actions/getSessionPayload'; import getUserAction from './actions/getUserAction'; import getUserProfileAction from './actions/getUserProfileAction'; +import getUserSchemaAction from './actions/getUserSchemaAction'; import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction'; import isSignedIn from './actions/isSignedIn'; import refreshToken from './actions/refreshToken'; @@ -126,6 +128,7 @@ const ThunderIDServerProvider: FC | null = null; const resolvedPreferences = { ...config?.preferences, @@ -169,9 +172,15 @@ const ThunderIDServerProvider: FC | null}; + error: string | null; + success: boolean; + } = await getUserSchemaAction(sessionId); user = userResponse.data?.user || {}; userProfile = userProfileResponse.data?.userProfile ?? userProfile; + userSchema = userSchemaResponse.data?.userSchema ?? null; } catch (error) { logger.warn('[ThunderIDServerProvider] Failed to fetch user profile from /users/me:', error?.toString()); } @@ -209,6 +218,7 @@ const ThunderIDServerProvider: FC diff --git a/packages/nextjs/src/server/actions/getUserSchemaAction.ts b/packages/nextjs/src/server/actions/getUserSchemaAction.ts new file mode 100644 index 00000000..3ff48348 --- /dev/null +++ b/packages/nextjs/src/server/actions/getUserSchemaAction.ts @@ -0,0 +1,30 @@ +// Copyright 2025 The ThunderID Authors +// SPDX-License-Identifier: Apache-2.0 + +'use server'; + +import {AttributeSchema} from '@thunderid/node'; +import getClient from '../getClient'; + +/** + * Server action to get the user attribute schema from `/users/me/meta`. + * Used to render profile fields dynamically (labels, required/regex validation) + * instead of falling back to raw, unlabeled attribute keys. + */ +const getUserSchemaAction = async ( + sessionId: string, +): Promise<{data: {userSchema: Record | null}; error: string | null; success: boolean}> => { + try { + const client = getClient(); + const userSchema: Record | null = await client.getUserSchema(sessionId); + return {data: {userSchema}, error: null, success: true}; + } catch (error) { + return { + data: {userSchema: null}, + error: 'Failed to get user schema', + success: false, + }; + } +}; + +export default getUserSchemaAction;