Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/nextjs/src/ThunderIDNextClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import {
ThunderIDNodeClient,
ThunderIDRuntimeError,
AttributeSchema,
AuthClientConfig,
EmbeddedSignInFlowResponse,
ExtendedAuthorizeRequestUrlParams,
Expand All @@ -19,6 +20,7 @@ import {
extractUserClaimsFromIdToken,
generateFlattenedUserProfile,
getUsersMe,
getUsersMeMeta,
updateMeProfile,
resolveResourceEndpoint,
} from '@thunderid/node';
Expand Down Expand Up @@ -154,6 +156,27 @@ class ThunderIDNextClient<T extends ThunderIDNextConfig = ThunderIDNextConfig> e
}
}

async getUserSchema(userId?: string): Promise<Record<string, AttributeSchema> | null> {
await this.ensureInitialized();

try {
const configData: AuthClientConfig<T> = 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<User> {
await this.ensureInitialized();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type UserProfileProps = Omit<BaseUserProfileProps, 'user' | 'profile' | '
*/
const UserProfile: FC<UserProfileProps> = ({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,
Expand Down Expand Up @@ -81,6 +81,7 @@ const UserProfile: FC<UserProfileProps> = ({preferences, editable = true, ...res
<BaseUserProfile
profile={profile!}
flattenedProfile={flattenedProfile!}
userSchema={userSchema}
editable={isEditableProfile}
onUpdate={isEditableProfile ? handleProfileUpdate : undefined}
error={error}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
'use client';

import {
AttributeSchema,
EmbeddedFlowExecuteRequestConfig,
FlowMetadataResponse,
generateFlattenedUserProfile,
Expand Down Expand Up @@ -69,6 +70,7 @@ export type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps,
) => Promise<{data: {user: User}; error: string; success: boolean}>;
user: User | null;
userProfile: UserProfile;
userSchema?: Record<string, AttributeSchema> | null;
};

const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>> = ({
Expand All @@ -86,6 +88,7 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
signUpUrl,
user: _user,
userProfile: _userProfile,
userSchema: _userSchema = null,
updateProfile,
applicationId,
organizationHandle,
Expand All @@ -100,11 +103,16 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
const [isLoading, setIsLoading] = useState<boolean>(true);
const [user, setUser] = useState<User | null>(_user);
const [userProfile, setUserProfile] = useState<UserProfile>(_userProfile);
const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(_userSchema);

useEffect(() => {
setUserProfile(_userProfile);
}, [_userProfile]);

useEffect(() => {
setUserSchema(_userSchema);
}, [_userSchema]);

useEffect(() => {
setUser(_user);
}, [_user]);
Expand Down Expand Up @@ -368,7 +376,7 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
signUp: handleSignUp,
signUpUrl,
user,
userSchema: null,
userSchema,
vendor: getVendorPrefix(vendor),
}),
[
Expand All @@ -382,6 +390,7 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
signInUrl,
signUpUrl,
user,
userSchema,
initialMeta,
vendor,
],
Expand All @@ -398,7 +407,12 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
>
<ThemeProvider theme={preferences?.theme?.overrides} mode={getActiveTheme(preferences?.theme?.mode as any)}>
<FlowProvider>
<UserProvider profile={userProfile} onUpdateProfile={handleProfileUpdate} updateProfile={updateProfile}>
<UserProvider
profile={userProfile}
userSchema={userSchema}
onUpdateProfile={handleProfileUpdate}
updateProfile={updateProfile}
>
{children}
</UserProvider>
</FlowProvider>
Expand Down
10 changes: 10 additions & 0 deletions packages/nextjs/src/server/ThunderIDProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import {
ThunderIDRuntimeError,
AttributeSchema,
FlowMetadataResponse,
FlowMetaType,
IdToken,
Expand All @@ -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';
Expand Down Expand Up @@ -126,6 +128,7 @@ const ThunderIDServerProvider: FC<PropsWithChildren<ThunderIDServerProviderProps
flattenedProfile: {},
profile: {},
};
let userSchema: Record<string, AttributeSchema> | null = null;

const resolvedPreferences = {
...config?.preferences,
Expand Down Expand Up @@ -169,9 +172,15 @@ const ThunderIDServerProvider: FC<PropsWithChildren<ThunderIDServerProviderProps
error: string | null;
success: boolean;
} = await getUserProfileAction(sessionId);
const userSchemaResponse: {
data: {userSchema: Record<string, AttributeSchema> | 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());
}
Expand Down Expand Up @@ -209,6 +218,7 @@ const ThunderIDServerProvider: FC<PropsWithChildren<ThunderIDServerProviderProps
clientId={config?.clientId}
user={user}
userProfile={userProfile}
userSchema={userSchema}
updateProfile={updateUserProfileAction}
isSignedIn={signedIn}
>
Expand Down
30 changes: 30 additions & 0 deletions packages/nextjs/src/server/actions/getUserSchemaAction.ts
Original file line number Diff line number Diff line change
@@ -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<string, AttributeSchema> | null}; error: string | null; success: boolean}> => {
try {
const client = getClient();
const userSchema: Record<string, AttributeSchema> | 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;
Loading