Skip to content

Commit 184e280

Browse files
authored
Merge pull request #83 from janithjay/fix-nextjs-users-me-meta
Populate Next.js UserProfile fields from users/me/meta schema
2 parents 1093a85 + 45b808f commit 184e280

5 files changed

Lines changed: 81 additions & 3 deletions

File tree

packages/nextjs/src/ThunderIDNextClient.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import {
55
ThunderIDNodeClient,
66
ThunderIDRuntimeError,
7+
AttributeSchema,
78
AuthClientConfig,
89
EmbeddedSignInFlowResponse,
910
ExtendedAuthorizeRequestUrlParams,
@@ -19,6 +20,7 @@ import {
1920
extractUserClaimsFromIdToken,
2021
generateFlattenedUserProfile,
2122
getUsersMe,
23+
getUsersMeMeta,
2224
updateMeProfile,
2325
resolveResourceEndpoint,
2426
} from '@thunderid/node';
@@ -154,6 +156,27 @@ class ThunderIDNextClient<T extends ThunderIDNextConfig = ThunderIDNextConfig> e
154156
}
155157
}
156158

159+
async getUserSchema(userId?: string): Promise<Record<string, AttributeSchema> | null> {
160+
await this.ensureInitialized();
161+
162+
try {
163+
const configData: AuthClientConfig<T> = await this.getStorageManager().getConfigData();
164+
const baseUrl: string | undefined = configData?.baseUrl;
165+
166+
const {schema} = await getUsersMeMeta({
167+
baseUrl,
168+
url: resolveResourceEndpoint('usersMeMeta', configData),
169+
headers: {
170+
Authorization: `Bearer ${await this.getAccessToken(userId)}`,
171+
},
172+
});
173+
174+
return schema ?? null;
175+
} catch (error) {
176+
return null;
177+
}
178+
}
179+
157180
override async updateUserProfile(payload: any, userId?: string): Promise<User> {
158181
await this.ensureInitialized();
159182

packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export type UserProfileProps = Omit<BaseUserProfileProps, 'user' | 'profile' | '
4040
*/
4141
const UserProfile: FC<UserProfileProps> = ({preferences, editable = true, ...rest}: UserProfileProps): ReactElement => {
4242
const {preferences: contextPreferences} = useThunderID();
43-
const {profile, flattenedProfile, onUpdateProfile, updateProfile} = useUser();
43+
const {profile, flattenedProfile, onUpdateProfile, updateProfile, userSchema} = useUser();
4444
const resolvedPreferences = {
4545
...contextPreferences,
4646
...preferences,
@@ -81,6 +81,7 @@ const UserProfile: FC<UserProfileProps> = ({preferences, editable = true, ...res
8181
<BaseUserProfile
8282
profile={profile!}
8383
flattenedProfile={flattenedProfile!}
84+
userSchema={userSchema}
8485
editable={isEditableProfile}
8586
onUpdate={isEditableProfile ? handleProfileUpdate : undefined}
8687
error={error}

packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use client';
55

66
import {
7+
AttributeSchema,
78
EmbeddedFlowExecuteRequestConfig,
89
FlowMetadataResponse,
910
generateFlattenedUserProfile,
@@ -69,6 +70,7 @@ export type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps,
6970
) => Promise<{data: {user: User}; error: string; success: boolean}>;
7071
user: User | null;
7172
userProfile: UserProfile;
73+
userSchema?: Record<string, AttributeSchema> | null;
7274
};
7375

7476
const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>> = ({
@@ -86,6 +88,7 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
8688
signUpUrl,
8789
user: _user,
8890
userProfile: _userProfile,
91+
userSchema: _userSchema = null,
8992
updateProfile,
9093
applicationId,
9194
organizationHandle,
@@ -100,11 +103,16 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
100103
const [isLoading, setIsLoading] = useState<boolean>(true);
101104
const [user, setUser] = useState<User | null>(_user);
102105
const [userProfile, setUserProfile] = useState<UserProfile>(_userProfile);
106+
const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(_userSchema);
103107

104108
useEffect(() => {
105109
setUserProfile(_userProfile);
106110
}, [_userProfile]);
107111

112+
useEffect(() => {
113+
setUserSchema(_userSchema);
114+
}, [_userSchema]);
115+
108116
useEffect(() => {
109117
setUser(_user);
110118
}, [_user]);
@@ -368,7 +376,7 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
368376
signUp: handleSignUp,
369377
signUpUrl,
370378
user,
371-
userSchema: null,
379+
userSchema,
372380
vendor: getVendorPrefix(vendor),
373381
}),
374382
[
@@ -382,6 +390,7 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
382390
signInUrl,
383391
signUpUrl,
384392
user,
393+
userSchema,
385394
initialMeta,
386395
vendor,
387396
],
@@ -398,7 +407,12 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
398407
>
399408
<ThemeProvider theme={preferences?.theme?.overrides} mode={getActiveTheme(preferences?.theme?.mode as any)}>
400409
<FlowProvider>
401-
<UserProvider profile={userProfile} onUpdateProfile={handleProfileUpdate} updateProfile={updateProfile}>
410+
<UserProvider
411+
profile={userProfile}
412+
userSchema={userSchema}
413+
onUpdateProfile={handleProfileUpdate}
414+
updateProfile={updateProfile}
415+
>
402416
{children}
403417
</UserProvider>
404418
</FlowProvider>

packages/nextjs/src/server/ThunderIDProvider.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import {
77
ThunderIDRuntimeError,
8+
AttributeSchema,
89
FlowMetadataResponse,
910
FlowMetaType,
1011
IdToken,
@@ -21,6 +22,7 @@ import getSessionId from './actions/getSessionId';
2122
import getSessionPayload from './actions/getSessionPayload';
2223
import getUserAction from './actions/getUserAction';
2324
import getUserProfileAction from './actions/getUserProfileAction';
25+
import getUserSchemaAction from './actions/getUserSchemaAction';
2426
import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction';
2527
import isSignedIn from './actions/isSignedIn';
2628
import refreshToken from './actions/refreshToken';
@@ -126,6 +128,7 @@ const ThunderIDServerProvider: FC<PropsWithChildren<ThunderIDServerProviderProps
126128
flattenedProfile: {},
127129
profile: {},
128130
};
131+
let userSchema: Record<string, AttributeSchema> | null = null;
129132

130133
const resolvedPreferences = {
131134
...config?.preferences,
@@ -169,9 +172,15 @@ const ThunderIDServerProvider: FC<PropsWithChildren<ThunderIDServerProviderProps
169172
error: string | null;
170173
success: boolean;
171174
} = await getUserProfileAction(sessionId);
175+
const userSchemaResponse: {
176+
data: {userSchema: Record<string, AttributeSchema> | null};
177+
error: string | null;
178+
success: boolean;
179+
} = await getUserSchemaAction(sessionId);
172180

173181
user = userResponse.data?.user || {};
174182
userProfile = userProfileResponse.data?.userProfile ?? userProfile;
183+
userSchema = userSchemaResponse.data?.userSchema ?? null;
175184
} catch (error) {
176185
logger.warn('[ThunderIDServerProvider] Failed to fetch user profile from /users/me:', error?.toString());
177186
}
@@ -209,6 +218,7 @@ const ThunderIDServerProvider: FC<PropsWithChildren<ThunderIDServerProviderProps
209218
clientId={config?.clientId}
210219
user={user}
211220
userProfile={userProfile}
221+
userSchema={userSchema}
212222
updateProfile={updateUserProfileAction}
213223
isSignedIn={signedIn}
214224
>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2025 The ThunderID Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
'use server';
5+
6+
import {AttributeSchema} from '@thunderid/node';
7+
import getClient from '../getClient';
8+
9+
/**
10+
* Server action to get the user attribute schema from `/users/me/meta`.
11+
* Used to render profile fields dynamically (labels, required/regex validation)
12+
* instead of falling back to raw, unlabeled attribute keys.
13+
*/
14+
const getUserSchemaAction = async (
15+
sessionId: string,
16+
): Promise<{data: {userSchema: Record<string, AttributeSchema> | null}; error: string | null; success: boolean}> => {
17+
try {
18+
const client = getClient();
19+
const userSchema: Record<string, AttributeSchema> | null = await client.getUserSchema(sessionId);
20+
return {data: {userSchema}, error: null, success: true};
21+
} catch (error) {
22+
return {
23+
data: {userSchema: null},
24+
error: 'Failed to get user schema',
25+
success: false,
26+
};
27+
}
28+
};
29+
30+
export default getUserSchemaAction;

0 commit comments

Comments
 (0)