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
5 changes: 5 additions & 0 deletions .changeset/cyan-spiders-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@asgardeo/react': patch
---

Fix UserProfile component in AsgardeoV2 platform
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,35 @@ const useStyles = (theme: Theme, colorScheme: string) => {
color: ${theme.vars.colors.text.primary};
`;

const profileSummary = css`
display: flex;
flex-direction: column;
align-items: flex-start;
`;

const sectionRow = css`
display: flex;
align-items: center;
padding: calc(${theme.vars.spacing.unit} * 1) 0;
`;

const sectionLabel = css`
font-size: 0.875rem;
font-weight: 600;
color: ${theme.vars.colors.text.primary};
width: 160px;
flex-shrink: 0;
`;

const sectionValue = css`
flex: 1;
display: flex;
align-items: center;
gap: calc(${theme.vars.spacing.unit} * 1.5);
font-size: 0.875rem;
color: ${theme.vars.colors.text.primary};
`;

const infoContainer = css`
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -186,6 +215,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
card,
header,
profileInfo,
profileSummary,
name,
infoContainer,
info,
Expand All @@ -201,6 +231,9 @@ const useStyles = (theme: Theme, colorScheme: string) => {
complexTextarea,
objectKey,
objectValue,
sectionRow,
sectionLabel,
sectionValue,
};
}, [
theme.vars.colors.background.surface,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import Card from '../../primitives/Card/Card';
import useStyles from './BaseUserProfile.styles';
import useTranslation from '../../../hooks/useTranslation';
import Alert from '../../primitives/Alert/Alert';
import Divider from '../../primitives/Divider/Divider';
import Typography from '../../primitives/Typography/Typography';
import getDisplayName from '../../../utils/getDisplayName';

interface ExtendedFlatSchema {
Expand Down Expand Up @@ -79,6 +81,7 @@ export interface BaseUserProfileProps {
title?: string;
error?: string | null;
isLoading?: boolean;
displayNameAttributes?: string[];
}

// Fields to skip based on schema.name
Expand Down Expand Up @@ -124,6 +127,7 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
isLoading = false,
showFields = [],
hideFields = [],
displayNameAttributes = [],
}): ReactElement => {
const {theme, colorScheme} = useTheme();
const [editedUser, setEditedUser] = useState(flattenedProfile || profile);
Expand Down Expand Up @@ -308,7 +312,7 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
picture: ['profile', 'profileUrl', 'picture', 'URL'],
firstName: ['name.givenName', 'given_name'],
lastName: ['name.familyName', 'family_name'],
email: ['emails'],
email: ['emails', 'email'],
username: ['userName', 'username', 'user_name'],
};

Expand Down Expand Up @@ -596,22 +600,45 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
const renderProfileWithoutSchemas = () => {
if (!currentUser) return null;

const displayName = getDisplayName(mergedMappings, profile, displayNameAttributes);

const profileEntries = Object.entries(currentUser)
.filter(([key, value]) => {
if (!shouldShowField(key)) return false;

return value !== undefined && value !== '' && value !== null;
})
.sort(([a], [b]) => a.localeCompare(b)); // Sort alphabetically
.sort(([a], [b]) => a.localeCompare(b));

return (
<>
{profileEntries.map(([key, value]) => (
<div key={key} className={styles.field}>
<span className={styles.label}>{formatLabel(key)}</span>
<div className={styles.value}>
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
<div className={styles.profileSummary}>
<Avatar
imageUrl={getMappedUserProfileValue('picture', mergedMappings, currentUser)}
name={displayName}
size={70}
alt={`${displayName}'s avatar`}
isLoading={isLoading}
/>
<Typography variant="h3" fontWeight="medium">
{displayName}
</Typography>
{getMappedUserProfileValue('email', mergedMappings, currentUser) && (
<Typography variant="body2" color="textSecondary">
{getMappedUserProfileValue('email', mergedMappings, currentUser)}
</Typography>
)}
</div>
<Divider />
{profileEntries.map(([key, value], index) => (
<div key={key}>
<div className={styles.sectionRow}>
<div className={styles.sectionLabel}>{formatLabel(key)}</div>
<div className={styles.sectionValue}>
{typeof value === 'object' ? <ObjectDisplay data={value} /> : String(value)}
</div>
</div>
{index < profileEntries.length - 1 && <Divider />}
</div>
))}
</>
Expand All @@ -626,15 +653,17 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
<Alert.Description>{error}</Alert.Description>
</Alert>
)}
<div className={styles.header}>
<Avatar
imageUrl={getMappedUserProfileValue('picture', mergedMappings, currentUser)}
name={getDisplayName(mergedMappings, profile)}
size={80}
alt={`${getDisplayName(mergedMappings, profile)}'s avatar`}
isLoading={isLoading}
/>
</div>
{schemas && schemas.length > 0 && (
<div className={styles.header}>
<Avatar
imageUrl={getMappedUserProfileValue('picture', mergedMappings, currentUser)}
name={getDisplayName(mergedMappings, profile)}
size={80}
alt={`${getDisplayName(mergedMappings, profile)}'s avatar`}
isLoading={isLoading}
/>
</div>
)}
<div className={styles.infoContainer}>
{schemas && schemas.length > 0
? schemas
Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,13 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
// TEMPORARY: Asgardeo V2 platform does not support SCIM2, Organizations endpoints yet.
// Tracker: https://github.com/asgardeo/javascript/issues/212
if (config.platform === Platform.AsgardeoV2) {
setUser(extractUserClaimsFromIdToken(decodedToken));
const claims = extractUserClaimsFromIdToken(decodedToken);
setUser(claims);
setUserProfile({
profile: claims as User,
flattenedProfile: claims as User,
schemas: [],
});
} else {
try {
const user: User = await asgardeo.getUser({baseUrl: _baseUrl});
Expand Down
38 changes: 24 additions & 14 deletions packages/react/src/utils/getDisplayName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,39 @@ import getMappedUserProfileValue from './getMappedUserProfileValue';
*
* @param mergedMappings - The merged attribute mappings.
* @param user - The user object containing profile information.
* @param displayAttributes - Optional array of attribute keys or paths to try first.
* Each entry is resolved via `getMappedUserProfileValue`. The first non-empty
* value found is returned. If none resolve, the default fallback chain is used.
*
* @example
* ```ts
* const mergedMappings = {
* firstName: ['name.givenName', 'given_name'],
* lastName: ['name.familyName', 'family_name'],
* username: ['userName', 'username', 'user_name'],
* email: ['emails[0].value', 'email'],
* name: ['name', 'fullName'],
* };
* // Default behavior — tries firstName+lastName, then username, email, name
* const displayName = getDisplayName(mergedMappings, user);
*
* const user: User = {
* id: '1',
* name: 'John Doe',
* email: 'john.doe@example.com',
* };
* // Custom attributes — try 'nickname' first, then fall back to defaults
* const displayName = getDisplayName(mergedMappings, user, ['nickname']);
*
* const displayName = getDisplayName(mergedMappings, user);
* // Multiple custom attributes
* const displayName = getDisplayName(mergedMappings, user, ['preferred_username', 'nickname']);
* ```
*
* @returns The display name of the user.
*/
const getDisplayName = (mergedMappings: {[key: string]: string | string[] | undefined}, user: User): string => {
const getDisplayName = (
mergedMappings: {[key: string]: string | string[] | undefined},
user: User,
displayAttributes?: string[],
): string => {
if (displayAttributes && displayAttributes.length > 0) {
for (const attr of displayAttributes) {
const value = getMappedUserProfileValue(attr, mergedMappings, user);

if (value !== undefined && value !== null && value !== '') {
return String(value);
}
}
}

const firstName = getMappedUserProfileValue('firstName', mergedMappings, user);
const lastName = getMappedUserProfileValue('lastName', mergedMappings, user);

Expand Down
Loading