+ )
+ : undefined;
+
+ const refreshHandler = async () => {
+ await fetchPaginatedOrganizations({
+ filter,
+ limit,
+ recursive,
+ reset: true,
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default OrganizationList;
diff --git a/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx b/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx
new file mode 100644
index 00000000..702f0f1f
--- /dev/null
+++ b/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx
@@ -0,0 +1,221 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use client';
+
+import {FC, ReactElement, useEffect, useState} from 'react';
+import {BaseOrganizationProfile, BaseOrganizationProfileProps, useTranslation} from '@asgardeo/react';
+import {OrganizationDetails, getOrganization, updateOrganization, createPatchOperations} from '@asgardeo/node';
+import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo';
+import getOrganizationAction from '../../../../server/actions/getOrganizationAction';
+import getSessionId from '../../../../server/actions/getSessionId';
+
+/**
+ * Props for the OrganizationProfile component.
+ * Extends BaseOrganizationProfileProps but makes the organization prop optional
+ * since it will be fetched using the organizationId
+ */
+export type OrganizationProfileProps = Omit & {
+ /**
+ * Component to show when there's an error loading organization data.
+ */
+ errorFallback?: ReactElement;
+
+ /**
+ * Component to show while loading organization data.
+ */
+ loadingFallback?: ReactElement;
+
+ /**
+ * Display mode for the component.
+ */
+ mode?: 'default' | 'popup';
+
+ /**
+ * Callback fired when the popup should be closed (only used in popup mode).
+ */
+ onOpenChange?: (open: boolean) => void;
+
+ /**
+ * Callback fired when the organization should be updated.
+ */
+ onUpdate?: (payload: any) => Promise;
+
+ /**
+ * Whether the popup is open (only used in popup mode).
+ */
+ open?: boolean;
+
+ /**
+ * The ID of the organization to fetch and display.
+ */
+ organizationId: string;
+
+ /**
+ * Custom title for the popup dialog (only used in popup mode).
+ */
+ popupTitle?: string;
+};
+
+/**
+ * OrganizationProfile component displays organization information in a
+ * structured and styled format. It automatically fetches organization details
+ * using the provided organization ID and displays them using BaseOrganizationProfile.
+ *
+ * The component supports editing functionality, allowing users to modify organization
+ * fields inline. Updates are automatically synced with the backend via the SCIM2 API.
+ *
+ * This component is the React-specific implementation that automatically
+ * retrieves the organization data from Asgardeo API.
+ *
+ * @example
+ * ```tsx
+ * // Basic usage with editing enabled (default)
+ *
+ *
+ * // Read-only mode
+ *
+ *
+ * // With card layout and custom fallbacks
+ * Loading organization...}
+ * errorFallback={
,
+ ...rest
+}: OrganizationProfileProps): ReactElement => {
+ const {baseUrl} = useAsgardeo();
+ const {t} = useTranslation();
+ const [organization, setOrganization] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+
+ const fetchOrganization = async () => {
+ if (!baseUrl || !organizationId) {
+ setLoading(false);
+ setError(true);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError(false);
+ const result = await getOrganizationAction(organizationId, (await getSessionId()) as string);
+
+ if (result.data?.organization) {
+ setOrganization(result.data.organization);
+
+ return;
+ }
+
+ setError(true);
+ } catch (err) {
+ console.error('Failed to fetch organization:', err);
+ setError(true);
+ setOrganization(null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchOrganization();
+ }, [baseUrl, organizationId]);
+
+ const handleOrganizationUpdate = async (payload: any): Promise => {
+ if (!baseUrl || !organizationId) return;
+
+ try {
+ // Convert payload to patch operations format
+ const operations = createPatchOperations(payload);
+
+ await updateOrganization({
+ baseUrl,
+ organizationId,
+ operations,
+ });
+ // Refetch organization data after update
+ await fetchOrganization();
+
+ // Call the optional onUpdate callback
+ if (onUpdate) {
+ await onUpdate(payload);
+ }
+ } catch (err) {
+ console.error('Failed to update organization:', err);
+ throw err;
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default OrganizationProfile;
diff --git a/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx
new file mode 100644
index 00000000..b840e005
--- /dev/null
+++ b/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -0,0 +1,198 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use client';
+
+import {FC, ReactElement, useState} from 'react';
+import {
+ BaseOrganizationSwitcher,
+ BaseOrganizationSwitcherProps,
+ BuildingAlt,
+ useOrganization,
+ useTranslation,
+} from '@asgardeo/react';
+import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo';
+import {CreateOrganization} from '../CreateOrganization/CreateOrganization';
+import OrganizationProfile from '../OrganizationProfile/OrganizationProfile';
+import OrganizationList from '../OrganizationList/OrganizationList';
+import {Organization} from '@asgardeo/node';
+
+/**
+ * Props interface for the OrganizationSwitcher component.
+ * Makes organizations optional since they'll be retrieved from OrganizationContext.
+ */
+export interface OrganizationSwitcherProps
+ extends Omit {
+ /**
+ * Optional override for current organization (will use context if not provided)
+ */
+ currentOrganization?: Organization;
+ /**
+ * Fallback element to render when the user is not signed in.
+ */
+ fallback?: ReactElement;
+ /**
+ * Optional callback for organization switch (will use context if not provided)
+ */
+ onOrganizationSwitch?: (organization: Organization) => Promise | void;
+ /**
+ * Optional override for organizations list (will use context if not provided)
+ */
+ organizations?: Organization[];
+}
+
+/**
+ * OrganizationSwitcher component that provides organization switching functionality.
+ * This component automatically retrieves organizations from the OrganizationContext.
+ * You can also override the organizations, currentOrganization, and onOrganizationSwitch
+ * by passing them as props.
+ *
+ * @example
+ * ```tsx
+ * import { OrganizationSwitcher } from '@asgardeo/react';
+ *
+ * // Basic usage - uses OrganizationContext
+ *
+ *
+ * // With custom organization switch handler
+ * {
+ * console.log('Switching to:', org.name);
+ * // Custom logic here
+ * }}
+ * />
+ *
+ * // With fallback for unauthenticated users
+ * Please sign in to view organizations}
+ * />
+ * ```
+ */
+export const OrganizationSwitcher: FC = ({
+ currentOrganization: propCurrentOrganization,
+ fallback = <>>,
+ onOrganizationSwitch: propOnOrganizationSwitch,
+ organizations: propOrganizations,
+ ...props
+}: OrganizationSwitcherProps): ReactElement => {
+ const {isSignedIn} = useAsgardeo();
+ const {
+ currentOrganization: contextCurrentOrganization,
+ organizations: contextOrganizations,
+ switchOrganization,
+ isLoading,
+ error,
+ } = useOrganization();
+ const [isCreateOrgOpen, setIsCreateOrgOpen] = useState(false);
+ const [isProfileOpen, setIsProfileOpen] = useState(false);
+ const [isOrganizationListOpen, setIsOrganizationListOpen] = useState(false);
+ const {t} = useTranslation();
+
+ if (!isSignedIn && fallback) {
+ return fallback;
+ }
+
+ if (!isSignedIn) {
+ return <>>;
+ }
+
+ const organizations: Organization[] = propOrganizations || contextOrganizations || [];
+ const currentOrganization: Organization = propCurrentOrganization || (contextCurrentOrganization as Organization);
+ const onOrganizationSwitch: (organization: Organization) => void = propOnOrganizationSwitch || switchOrganization;
+
+ const handleManageOrganizations = (): void => {
+ setIsOrganizationListOpen(true);
+ };
+
+ const handleManageOrganization = (): void => {
+ setIsProfileOpen(true);
+ };
+
+ const defaultMenuItems: Array<{icon?: ReactElement; label: string; onClick: () => void}> = [];
+
+ if (currentOrganization) {
+ defaultMenuItems.push({
+ icon: ,
+ label: t('organization.switcher.manage.organizations'),
+ onClick: handleManageOrganizations,
+ });
+ }
+
+ defaultMenuItems.push({
+ icon: (
+
+ ),
+ label: t('organization.switcher.create.organization'),
+ onClick: (): void => setIsCreateOrgOpen(true),
+ });
+
+ const menuItems = props.menuItems ? [...defaultMenuItems, ...props.menuItems] : defaultMenuItems;
+
+ return (
+ <>
+
+ {
+ if (org && onOrganizationSwitch) {
+ onOrganizationSwitch(org);
+ }
+ setIsCreateOrgOpen(false);
+ }}
+ />
+ {currentOrganization && (
+ {t('organization.profile.loading')}}
+ errorFallback={
{t('organization.profile.error')}
}
+ />
+ )}
+ {
+ if (onOrganizationSwitch) {
+ onOrganizationSwitch(organization);
+ }
+ setIsOrganizationListOpen(false);
+ }}
+ />
+ >
+ );
+};
+
+export default OrganizationSwitcher;
diff --git a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx
index 73c24427..6465b4e7 100644
--- a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx
+++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx
@@ -23,7 +23,7 @@ import {BaseUserProfile, BaseUserProfileProps, useUser} from '@asgardeo/react';
import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo';
import getSessionId from '../../../../server/actions/getSessionId';
import updateUserProfileAction from '../../../../server/actions/updateUserProfileAction';
-import { Schema, User } from '@asgardeo/node';
+import {Schema, User} from '@asgardeo/node';
/**
* Props for the UserProfile component.
@@ -56,11 +56,11 @@ export type UserProfileProps = Omit = ({...rest}: UserProfileProps): ReactElement => {
const {baseUrl} = useAsgardeo();
- const {profile, flattenedProfile, schemas, revalidateProfile} = useUser();
+ const {profile, flattenedProfile, schemas, onUpdateProfile, updateProfile} = useUser();
const handleProfileUpdate = async (payload: any): Promise => {
- await updateUserProfileAction(payload, (await getSessionId()) as string);
- await revalidateProfile();
+ const result = await updateProfile(payload, (await getSessionId()) as string);
+ onUpdateProfile(result?.data?.user);
};
return (
diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts
index c1fc018c..a0658be0 100644
--- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts
+++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts
@@ -31,6 +31,8 @@ export type AsgardeoContextProps = Partial;
* Context object for managing the Authentication flow builder core context.
*/
const AsgardeoContext: Context = createContext({
+ organizationHandle: undefined,
+ applicationId: undefined,
signInUrl: undefined,
signUpUrl: undefined,
afterSignInUrl: undefined,
diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx
index cc0dcbe2..e03cb254 100644
--- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx
+++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx
@@ -19,16 +19,30 @@
'use client';
import {
+ AsgardeoRuntimeError,
EmbeddedFlowExecuteRequestConfig,
EmbeddedFlowExecuteRequestPayload,
EmbeddedSignInFlowHandleRequestPayload,
+ generateFlattenedUserProfile,
+ Organization,
+ UpdateMeProfileConfig,
User,
UserProfile,
} from '@asgardeo/node';
-import {I18nProvider, FlowProvider, UserProvider, ThemeProvider, AsgardeoProviderProps} from '@asgardeo/react';
-import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react';
+import {
+ I18nProvider,
+ FlowProvider,
+ UserProvider,
+ ThemeProvider,
+ AsgardeoProviderProps,
+ OrganizationProvider,
+} from '@asgardeo/react';
+import {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react';
import {useRouter, useSearchParams} from 'next/navigation';
import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext';
+import getOrganizationsAction from '../../../server/actions/getOrganizationsAction';
+import getSessionId from '../../../server/actions/getSessionId';
+import switchOrganizationAction from '../../../server/actions/switchOrganizationAction';
/**
* Props interface of {@link AsgardeoClientProvider}
@@ -38,10 +52,21 @@ export type AsgardeoClientProviderProps = Partial Promise<{success: boolean; error?: string; redirectUrl?: string}>;
+ applicationId: AsgardeoContextProps['applicationId'];
+ organizationHandle: AsgardeoContextProps['organizationHandle'];
+ handleOAuthCallback: (
+ code: string,
+ state: string,
+ sessionState?: string,
+ ) => Promise<{success: boolean; error?: string; redirectUrl?: string}>;
isSignedIn: boolean;
userProfile: UserProfile;
+ currentOrganization: Organization;
user: User | null;
+ updateProfile: (
+ requestConfig: UpdateMeProfileConfig,
+ sessionId?: string,
+ ) => Promise<{success: boolean; data: {user: User}; error: string}>;
};
const AsgardeoClientProvider: FC> = ({
@@ -55,14 +80,28 @@ const AsgardeoClientProvider: FC>
isSignedIn,
signInUrl,
signUpUrl,
- user,
- userProfile,
+ user: _user,
+ userProfile: _userProfile,
+ currentOrganization,
+ updateProfile,
+ applicationId,
+ organizationHandle,
}: PropsWithChildren) => {
+ const reRenderCheckRef: RefObject = useRef(false);
const router = useRouter();
const searchParams = useSearchParams();
const [isDarkMode, setIsDarkMode] = useState(false);
const [isLoading, setIsLoading] = useState(true);
- const [_userProfile, setUserProfile] = useState(userProfile);
+ const [user, setUser] = useState(_user);
+ const [userProfile, setUserProfile] = useState(_userProfile);
+
+ useEffect(() => {
+ setUserProfile(_userProfile);
+ }, [_userProfile]);
+
+ useEffect(() => {
+ setUser(_user);
+ }, [_user]);
// Handle OAuth callback automatically
useEffect(() => {
@@ -81,7 +120,11 @@ const AsgardeoClientProvider: FC>
if (error) {
console.error('[AsgardeoClientProvider] OAuth error:', error, errorDescription);
// Redirect to sign-in page with error
- router.push(`/signin?error=${encodeURIComponent(error)}&error_description=${encodeURIComponent(errorDescription || '')}`);
+ router.push(
+ `/signin?error=${encodeURIComponent(error)}&error_description=${encodeURIComponent(
+ errorDescription || '',
+ )}`,
+ );
return;
}
@@ -100,7 +143,11 @@ const AsgardeoClientProvider: FC>
window.location.reload();
}
} else {
- router.push(`/signin?error=authentication_failed&error_description=${encodeURIComponent(result.error || 'Authentication failed')}`);
+ router.push(
+ `/signin?error=authentication_failed&error_description=${encodeURIComponent(
+ result.error || 'Authentication failed',
+ )}`,
+ );
}
}
} catch (error) {
@@ -206,6 +253,25 @@ const AsgardeoClientProvider: FC>
}
};
+ const switchOrganization = async (organization: Organization): Promise => {
+ try {
+ await switchOrganizationAction(organization, (await getSessionId()) as string);
+
+ // if (await asgardeo.isSignedIn()) {
+ // setUser(await asgardeo.getUser());
+ // setUserProfile(await asgardeo.getUserProfile());
+ // setCurrentOrganization(await asgardeo.getCurrentOrganization());
+ // }
+ } catch (error) {
+ throw new AsgardeoRuntimeError(
+ `Failed to switch organization: ${error instanceof Error ? error.message : String(error)}`,
+ 'AsgardeoClientProvider-switchOrganization-RuntimeError-001',
+ 'nextjs',
+ 'An error occurred while switching to the specified organization.',
+ );
+ }
+ };
+
const contextValue = useMemo(
() => ({
baseUrl,
@@ -217,16 +283,39 @@ const AsgardeoClientProvider: FC>
signUp: handleSignUp,
signInUrl,
signUpUrl,
+ applicationId,
+ organizationHandle,
}),
- [baseUrl, user, isSignedIn, isLoading, signInUrl, signUpUrl],
+ [baseUrl, user, isSignedIn, isLoading, signInUrl, signUpUrl, applicationId, organizationHandle],
);
+ const handleProfileUpdate = (payload: User): void => {
+ setUser(payload);
+ setUserProfile(prev => ({
+ ...prev,
+ profile: payload,
+ flattenedProfile: generateFlattenedUserProfile(payload, prev?.schemas),
+ }));
+ };
+
return (
-
+
- {children}
+
+ {
+ const result = await getOrganizationsAction((await getSessionId()) as string);
+
+ return result?.data?.organizations || [];
+ }}
+ currentOrganization={currentOrganization}
+ onOrganizationSwitch={switchOrganization}
+ >
+ {children}
+
+
diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts
index ae8cccb6..40ceb844 100644
--- a/packages/nextjs/src/index.ts
+++ b/packages/nextjs/src/index.ts
@@ -26,6 +26,15 @@ export {default as isSignedIn} from './server/actions/isSignedIn';
export {default as handleOAuthCallback} from './server/actions/handleOAuthCallbackAction';
+export {default as CreateOrganization} from './client/components/presentation/CreateOrganization/CreateOrganization';
+export {CreateOrganizationProps} from './client/components/presentation/CreateOrganization/CreateOrganization';
+
+export {default as OrganizationProfile} from './client/components/presentation/OrganizationProfile/OrganizationProfile';
+export {OrganizationProfileProps} from './client/components/presentation/OrganizationProfile/OrganizationProfile';
+
+export {default as OrganizationSwitcher} from './client/components/presentation/OrganizationSwitcher/OrganizationSwitcher';
+export {OrganizationSwitcherProps} from './client/components/presentation/OrganizationSwitcher/OrganizationSwitcher';
+
export {default as SignedIn} from './client/components/control/SignedIn/SignedIn';
export {SignedInProps} from './client/components/control/SignedIn/SignedIn';
diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx
index 21efda1d..dea32dc1 100644
--- a/packages/nextjs/src/server/AsgardeoProvider.tsx
+++ b/packages/nextjs/src/server/AsgardeoProvider.tsx
@@ -16,9 +16,11 @@
* under the License.
*/
+'use server';
+
import {FC, PropsWithChildren, ReactElement} from 'react';
-import {AsgardeoRuntimeError, User, UserProfile} from '@asgardeo/node';
-import AsgardeoClientProvider, {AsgardeoClientProviderProps} from '../client/contexts/Asgardeo/AsgardeoProvider';
+import {AsgardeoRuntimeError, Organization, User, UserProfile} from '@asgardeo/node';
+import AsgardeoClientProvider from '../client/contexts/Asgardeo/AsgardeoProvider';
import AsgardeoNextClient from '../AsgardeoNextClient';
import signInAction from './actions/signInAction';
import signOutAction from './actions/signOutAction';
@@ -30,6 +32,8 @@ import getUserProfileAction from './actions/getUserProfileAction';
import signUpAction from './actions/signUpAction';
import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction';
import {AsgardeoProviderProps} from '@asgardeo/react';
+import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction';
+import updateUserProfileAction from './actions/updateUserProfileAction';
/**
* Props interface of {@link AsgardeoServerProvider}
@@ -88,28 +92,39 @@ const AsgardeoServerProvider: FC>
profile: {},
flattenedProfile: {},
};
+ let currentOrganization: Organization = {
+ id: '',
+ name: '',
+ orgHandle: '',
+ };
if (_isSignedIn) {
const userResponse = await getUserAction(sessionId);
const userProfileResponse = await getUserProfileAction(sessionId);
+ const currentOrganizationResponse = await getCurrentOrganizationAction(sessionId);
user = userResponse.data?.user || {};
userProfile = userProfileResponse.data?.userProfile;
+ currentOrganization = currentOrganizationResponse?.data?.organization as Organization;
}
return (
{children}
diff --git a/packages/nextjs/src/server/actions/createOrganizationAction.ts b/packages/nextjs/src/server/actions/createOrganizationAction.ts
new file mode 100644
index 00000000..83a1b0fc
--- /dev/null
+++ b/packages/nextjs/src/server/actions/createOrganizationAction.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use server';
+
+import {CreateOrganizationPayload, Organization} from '@asgardeo/node';
+import AsgardeoNextClient from '../../AsgardeoNextClient';
+
+/**
+ * Server action to create an organization.
+ */
+const createOrganizationAction = async (payload: CreateOrganizationPayload, sessionId: string) => {
+ try {
+ const client = AsgardeoNextClient.getInstance();
+ const organization: Organization = await client.createOrganization(payload, sessionId);
+ return {success: true, data: {organization}, error: null};
+ } catch (error) {
+ return {
+ success: false,
+ data: {
+ user: {},
+ },
+ error: 'Failed to create organization',
+ };
+ }
+};
+
+export default createOrganizationAction;
diff --git a/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts b/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts
new file mode 100644
index 00000000..bd08d9c7
--- /dev/null
+++ b/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use server';
+
+import {Organization, OrganizationDetails} from '@asgardeo/node';
+import AsgardeoNextClient from '../../AsgardeoNextClient';
+
+/**
+ * Server action to create an organization.
+ */
+const getCurrentOrganizationAction = async (sessionId: string) => {
+ try {
+ const client = AsgardeoNextClient.getInstance();
+ const organization: Organization = await client.getCurrentOrganization(sessionId) as Organization;
+ return {success: true, data: {organization}, error: null};
+ } catch (error) {
+ return {
+ success: false,
+ data: {
+ user: {},
+ },
+ error: 'Failed to get the current organization',
+ };
+ }
+};
+
+export default getCurrentOrganizationAction;
diff --git a/packages/nextjs/src/server/actions/getOrganizationAction.ts b/packages/nextjs/src/server/actions/getOrganizationAction.ts
new file mode 100644
index 00000000..e5eb99d6
--- /dev/null
+++ b/packages/nextjs/src/server/actions/getOrganizationAction.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use server';
+
+import {OrganizationDetails} from '@asgardeo/node';
+import AsgardeoNextClient from '../../AsgardeoNextClient';
+
+/**
+ * Server action to create an organization.
+ */
+const getOrganizationAction = async (organizationId: string, sessionId: string) => {
+ try {
+ const client = AsgardeoNextClient.getInstance();
+ const organization: OrganizationDetails = await client.getOrganization(organizationId, sessionId);
+ return {success: true, data: {organization}, error: null};
+ } catch (error) {
+ return {
+ success: false,
+ data: {
+ user: {},
+ },
+ error: 'Failed to get organization',
+ };
+ }
+};
+
+export default getOrganizationAction;
diff --git a/packages/nextjs/src/server/actions/getOrganizationsAction.ts b/packages/nextjs/src/server/actions/getOrganizationsAction.ts
new file mode 100644
index 00000000..16c87807
--- /dev/null
+++ b/packages/nextjs/src/server/actions/getOrganizationsAction.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use server';
+
+import {Organization} from '@asgardeo/node';
+import AsgardeoNextClient from '../../AsgardeoNextClient';
+
+/**
+ * Server action to get organizations.
+ */
+const getOrganizationsAction = async (sessionId: string) => {
+ try {
+ const client = AsgardeoNextClient.getInstance();
+ const organizations: Organization[] = await client.getOrganizations(sessionId);
+ return {success: true, data: {organizations}, error: null};
+ } catch (error) {
+ return {
+ success: false,
+ data: {
+ user: {},
+ },
+ error: 'Failed to get organizations',
+ };
+ }
+};
+
+export default getOrganizationsAction;
diff --git a/packages/nextjs/src/server/actions/switchOrganizationAction.ts b/packages/nextjs/src/server/actions/switchOrganizationAction.ts
new file mode 100644
index 00000000..9b049bac
--- /dev/null
+++ b/packages/nextjs/src/server/actions/switchOrganizationAction.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use server';
+
+import {Organization, OrganizationDetails} from '@asgardeo/node';
+import AsgardeoNextClient from '../../AsgardeoNextClient';
+
+/**
+ * Server action to create an organization.
+ */
+const switchOrganizationAction = async (organization: Organization, sessionId: string) => {
+ try {
+ const client = AsgardeoNextClient.getInstance();
+ await client.switchOrganization(organization, sessionId);
+ return {success: true, error: null};
+ } catch (error) {
+ return {
+ success: false,
+ data: {
+ user: {},
+ },
+ error: 'Failed to switch to organization',
+ };
+ }
+};
+
+export default switchOrganizationAction;
diff --git a/packages/nextjs/src/server/actions/updateUserProfileAction.ts b/packages/nextjs/src/server/actions/updateUserProfileAction.ts
index dabda4f3..69ec47d5 100644
--- a/packages/nextjs/src/server/actions/updateUserProfileAction.ts
+++ b/packages/nextjs/src/server/actions/updateUserProfileAction.ts
@@ -18,25 +18,28 @@
'use server';
-import {User, UserProfile} from '@asgardeo/node';
+import {UpdateMeProfileConfig, User, UserProfile} from '@asgardeo/node';
import AsgardeoNextClient from '../../AsgardeoNextClient';
/**
* Server action to get the current user.
* Returns the user profile if signed in.
*/
-const updateUserProfileAction = async (payload: any, sessionId: string) => {
+const updateUserProfileAction = async (
+ payload: UpdateMeProfileConfig,
+ sessionId?: string,
+): Promise<{success: boolean; data: {user: User}; error: string}> => {
try {
const client = AsgardeoNextClient.getInstance();
const user: User = await client.updateUserProfile(payload, sessionId);
- return {success: true, data: {user}, error: null};
+ return {success: true, data: {user}, error: ""};
} catch (error) {
return {
success: false,
data: {
user: {},
},
- error: 'Failed to get user profile',
+ error: `Failed to get user profile: ${error instanceof Error ? error.message : String(error)}`,
};
}
};
diff --git a/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts b/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts
index 7c162519..2c25fa43 100644
--- a/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts
+++ b/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts
@@ -19,10 +19,12 @@
import {AsgardeoNextConfig} from '../models/config';
const decorateConfigWithNextEnv = (config: AsgardeoNextConfig): AsgardeoNextConfig => {
- const {baseUrl, clientId, clientSecret, signInUrl, signUpUrl, afterSignInUrl, afterSignOutUrl, ...rest} = config;
+ const {organizationHandle, applicationId, baseUrl, clientId, clientSecret, signInUrl, signUpUrl, afterSignInUrl, afterSignOutUrl, ...rest} = config;
return {
...rest,
+ organizationHandle: organizationHandle || (process.env['NEXT_PUBLIC_ASGARDEO_ORGANIZATION_HANDLE'] as string),
+ applicationId: applicationId || (process.env['NEXT_PUBLIC_ASGARDEO_APPLICATION_ID'] as string),
baseUrl: baseUrl || (process.env['NEXT_PUBLIC_ASGARDEO_BASE_URL'] as string),
clientId: clientId || (process.env['NEXT_PUBLIC_ASGARDEO_CLIENT_ID'] as string),
clientSecret: clientSecret || (process.env['ASGARDEO_CLIENT_SECRET'] as string),
diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts
index 19af191c..86f1b2e9 100644
--- a/packages/react/src/AsgardeoReactClient.ts
+++ b/packages/react/src/AsgardeoReactClient.ts
@@ -35,6 +35,7 @@ import {
Organization,
IdToken,
EmbeddedFlowExecuteRequestConfig,
+ deriveOrganizationHandleFromBaseUrl,
} from '@asgardeo/browser';
import AuthAPI from './__temp__/api';
import getMeOrganizations from './api/getMeOrganizations';
@@ -59,13 +60,27 @@ class AsgardeoReactClient e
}
override initialize(config: AsgardeoReactConfig): Promise {
- return this.asgardeo.init(config as any);
+ let resolvedOrganizationHandle: string | undefined = config?.organizationHandle;
+
+ if (!resolvedOrganizationHandle) {
+ resolvedOrganizationHandle = deriveOrganizationHandleFromBaseUrl(config?.baseUrl);
+ }
+
+ return this.asgardeo.init({...config, organizationHandle: resolvedOrganizationHandle} as any);
+ }
+
+ override async updateUserProfile(payload: any, userId?: string): Promise {
+ throw new Error('Not implemented');
}
- override async getUser(): Promise {
+ override async getUser(options?: any): Promise {
try {
- const configData = await this.asgardeo.getConfigData();
- const baseUrl = configData?.baseUrl;
+ let baseUrl = options?.baseUrl;
+
+ if (!baseUrl) {
+ const configData = await this.asgardeo.getConfigData();
+ baseUrl = configData?.baseUrl;
+ }
const profile = await getScim2Me({baseUrl});
const schemas = await getSchemas({baseUrl});
@@ -76,10 +91,18 @@ class AsgardeoReactClient e
}
}
- async getUserProfile(): Promise {
+ async getDecodedIdToken(sessionId?: string): Promise {
+ return this.asgardeo.getDecodedIdToken(sessionId);
+ }
+
+ async getUserProfile(options?: any): Promise {
try {
- const configData = await this.asgardeo.getConfigData();
- const baseUrl = configData?.baseUrl;
+ let baseUrl = options?.baseUrl;
+
+ if (!baseUrl) {
+ const configData = await this.asgardeo.getConfigData();
+ baseUrl = configData?.baseUrl;
+ }
const profile = await getScim2Me({baseUrl});
const schemas = await getSchemas({baseUrl});
@@ -102,10 +125,14 @@ class AsgardeoReactClient e
}
}
- override async getOrganizations(): Promise {
+ override async getOrganizations(options?: any): Promise {
try {
- const configData = await this.asgardeo.getConfigData();
- const baseUrl = configData?.baseUrl;
+ let baseUrl = options?.baseUrl;
+
+ if (!baseUrl) {
+ const configData = await this.asgardeo.getConfigData();
+ baseUrl = configData?.baseUrl;
+ }
const organizations = await getMeOrganizations({baseUrl});
@@ -211,7 +238,7 @@ class AsgardeoReactClient e
});
}
- return this.asgardeo.signIn(arg1 as any) as unknown as Promise;
+ return (await this.asgardeo.signIn(arg1 as any)) as unknown as Promise;
}
override signOut(options?: SignOutOptions, afterSignOut?: (afterSignOutUrl: string) => void): Promise;
diff --git a/packages/react/src/__temp__/api.ts b/packages/react/src/__temp__/api.ts
index 8db768dc..83e26768 100644
--- a/packages/react/src/__temp__/api.ts
+++ b/packages/react/src/__temp__/api.ts
@@ -296,8 +296,8 @@ class AuthAPI {
* @return {Promise} - A Promise that resolves with
* the decoded payload of the id token.
*/
- public async getDecodedIdToken(): Promise {
- return this._client.getDecodedIdToken();
+ public async getDecodedIdToken(sessionId?: string): Promise {
+ return this._client.getDecodedIdToken(sessionId);
}
/**
diff --git a/packages/react/src/__temp__/models.ts b/packages/react/src/__temp__/models.ts
index fc500cf4..537f7dc0 100644
--- a/packages/react/src/__temp__/models.ts
+++ b/packages/react/src/__temp__/models.ts
@@ -96,7 +96,7 @@ export interface AuthContextInterface {
getOpenIDProviderEndpoints(): Promise;
getHttpClient(): Promise;
getDecodedIDPIDToken(): Promise;
- getDecodedIdToken(): Promise;
+ getDecodedIdToken(sessionId?: string): Promise;
getIdToken(): Promise;
getAccessToken(): Promise;
refreshAccessToken(): Promise;
diff --git a/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx b/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx
index e7f6eb0b..9c75d066 100644
--- a/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx
+++ b/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx
@@ -23,7 +23,6 @@ import getOrganization from '../../../api/getOrganization';
import updateOrganization, {createPatchOperations} from '../../../api/updateOrganization';
import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo';
import useTranslation from '../../../hooks/useTranslation';
-import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover';
/**
* Props for the OrganizationProfile component.
@@ -201,33 +200,7 @@ const OrganizationProfile: FC = ({
}
};
- if (loading) {
- return mode === 'popup' ? (
-
- ) : (
- loadingFallback
- );
- }
-
- if (error) {
- return mode === 'popup' ? (
-
- ) : (
- errorFallback
- );
- }
-
- const profileContent = (
+ return (
= ({
{...rest}
/>
);
-
- return profileContent;
};
export default OrganizationProfile;
diff --git a/packages/react/src/components/presentation/SignIn/SignIn.tsx b/packages/react/src/components/presentation/SignIn/SignIn.tsx
index 8a170bae..da9c5b80 100644
--- a/packages/react/src/components/presentation/SignIn/SignIn.tsx
+++ b/packages/react/src/components/presentation/SignIn/SignIn.tsx
@@ -57,7 +57,7 @@ export type SignInProps = Pick = ({className, size = 'medium', variant = 'outlined'}: SignInProps) => {
+const SignIn: FC = ({className, size = 'medium', ...rest}: SignInProps) => {
const {signIn, afterSignInUrl, isInitialized, isLoading} = useAsgardeo();
/**
@@ -103,7 +103,7 @@ const SignIn: FC = ({className, size = 'medium', variant = 'outline
onSuccess={handleSuccess}
className={className}
size={size}
- variant={variant}
+ {...rest}
/>
);
};
diff --git a/packages/react/src/components/presentation/SignUp/SignUp.tsx b/packages/react/src/components/presentation/SignUp/SignUp.tsx
index 18766232..69642e41 100644
--- a/packages/react/src/components/presentation/SignUp/SignUp.tsx
+++ b/packages/react/src/components/presentation/SignUp/SignUp.tsx
@@ -64,11 +64,11 @@ export type SignUpProps = BaseSignUpProps;
const SignUp: FC = ({
className,
size = 'medium',
- variant = 'outlined',
afterSignUpUrl,
onError,
onComplete,
shouldRedirectAfterSignUp = true,
+ ...rest
}) => {
const {signUp, isInitialized} = useAsgardeo();
@@ -121,8 +121,8 @@ const SignUp: FC = ({
onComplete={handleComplete}
className={className}
size={size}
- variant={variant}
isInitialized={isInitialized}
+ {...rest}
/>
);
};
diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx
index 6158b3ff..50e59dd6 100644
--- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx
+++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx
@@ -27,6 +27,7 @@ import Checkbox from '../../primitives/Checkbox/Checkbox';
import DatePicker from '../../primitives/DatePicker/DatePicker';
import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover';
import TextField from '../../primitives/TextField/TextField';
+import MultiInput from '../../primitives/MultiInput/MultiInput';
import Card from '../../primitives/Card/Card';
interface ExtendedFlatSchema {
@@ -76,6 +77,31 @@ export interface BaseUserProfileProps {
title?: string;
}
+// Fields to skip based on schema.name
+const fieldsToSkip: string[] = [
+ 'roles.default',
+ 'active',
+ 'groups',
+ 'profileUrl',
+ 'accountLocked',
+ 'accountDisabled',
+ 'oneTimePassword',
+ 'userSourceId',
+ 'idpType',
+ 'localCredentialExists',
+ 'active',
+ 'ResourceType',
+ 'ExternalID',
+ 'MetaData',
+ 'verifiedMobileNumbers',
+ 'verifiedEmailAddresses',
+ 'phoneNumbers.mobile',
+ 'emailAddresses',
+];
+
+// Fields that should be readonly
+const readonlyFields: string[] = ['username', 'userName', 'user_name'];
+
const BaseUserProfile: FC = ({
fallback = null,
className = '',
@@ -188,13 +214,18 @@ const BaseUserProfile: FC = ({
if (!onUpdate || !schema.name) return;
const fieldName: string = schema.name;
- const fieldValue: any =
+ let fieldValue: any =
editedUser && fieldName && editedUser[fieldName] !== undefined
? editedUser[fieldName]
: flattenedProfile && flattenedProfile[fieldName] !== undefined
? flattenedProfile[fieldName]
: '';
+ // Filter out empty values for arrays when saving
+ if (Array.isArray(fieldValue)) {
+ fieldValue = fieldValue.filter(v => v !== undefined && v !== null && v !== '');
+ }
+
let payload: Record = {};
// SCIM Patch Operation Logic:
@@ -293,7 +324,7 @@ const BaseUserProfile: FC = ({
onStartEdit?: () => void,
): ReactElement | null => {
if (!schema) return null;
- const {value, displayName, description, name, type, required, mutability, subAttributes} = schema;
+ const {value, displayName, description, name, type, required, mutability, subAttributes, multiValued} = schema;
const label = displayName || description || name || '';
// If complex or subAttributes, fallback to original renderSchemaValue
@@ -317,13 +348,68 @@ const BaseUserProfile: FC = ({
>
);
}
- if (Array.isArray(value)) {
- const hasValues = value.length > 0;
- const isEditable = editable && mutability !== 'READ_ONLY';
+ // Handle multi-valued fields (either array values or multiValued property)
+ if (Array.isArray(value) || multiValued) {
+ const hasValues = Array.isArray(value) ? value.length > 0 : value !== undefined && value !== null && value !== '';
+ const isEditable = editable && mutability !== 'READ_ONLY' && !readonlyFields.includes(name || '');
+
+ // If editing, show multi-valued input
+ if (isEditing && onEditValue && isEditable) {
+ // Use editedUser value if available, then flattenedProfile, then schema value
+ const currentValue =
+ editedUser && name && editedUser[name] !== undefined
+ ? editedUser[name]
+ : flattenedProfile && name && flattenedProfile[name] !== undefined
+ ? flattenedProfile[name]
+ : value;
+
+ let fieldValues: string[];
+ if (Array.isArray(currentValue)) {
+ fieldValues = currentValue.map(String);
+ } else if (currentValue !== undefined && currentValue !== null && currentValue !== '') {
+ fieldValues = [String(currentValue)];
+ } else {
+ fieldValues = [];
+ }
+
+ return (
+ <>
+ {label}
+
+ {
+ // Don't filter out empty values during editing - only when saving
+ // This allows users to type and keeps empty fields for adding new values
+ if (multiValued || Array.isArray(currentValue)) {
+ onEditValue(newValues);
+ } else {
+ // Single value field, just take the first value (including empty for typing)
+ onEditValue(newValues[0] || '');
+ }
+ }}
+ placeholder={getFieldPlaceholder(schema)}
+ fieldType={type as 'STRING' | 'DATE_TIME' | 'BOOLEAN'}
+ type={type === 'DATE_TIME' ? 'date' : type === 'STRING' ? 'text' : 'text'}
+ required={required}
+ style={{
+ marginBottom: 0,
+ }}
+ />
+
+ >
+ );
+ }
+
+ // View mode for multi-valued fields
let displayValue: string;
if (hasValues) {
- displayValue = value.map(item => (typeof item === 'object' ? JSON.stringify(item) : String(item))).join(', ');
+ if (Array.isArray(value)) {
+ displayValue = value.map(item => (typeof item === 'object' ? JSON.stringify(item) : String(item))).join(', ');
+ } else {
+ displayValue = String(value);
+ }
} else if (isEditable) {
displayValue = getFieldPlaceholder(schema);
} else {
@@ -363,7 +449,7 @@ const BaseUserProfile: FC = ({
return ;
}
// If editing, show field instead of value
- if (isEditing && onEditValue && mutability !== 'READ_ONLY') {
+ if (isEditing && onEditValue && mutability !== 'READ_ONLY' && !readonlyFields.includes(name || '')) {
// Use editedUser value if available, then flattenedProfile, then schema value
const fieldValue =
editedUser && name && editedUser[name] !== undefined
@@ -425,7 +511,7 @@ const BaseUserProfile: FC = ({
}
// Default: view mode
const hasValue = value !== undefined && value !== null && value !== '';
- const isEditable = editable && mutability !== 'READ_ONLY';
+ const isEditable = editable && mutability !== 'READ_ONLY' && !readonlyFields.includes(name || '');
let displayValue: string;
if (hasValue) {
@@ -472,6 +558,7 @@ const BaseUserProfile: FC = ({
// Skip fields with undefined or empty values unless editing or editable
const hasValue = schema.value !== undefined && schema.value !== '' && schema.value !== null;
const isFieldEditing = editingFields[schema.name];
+ const isReadonlyField = readonlyFields.includes(schema.name);
// Show field if: has value, currently editing, or is editable and READ_WRITE
const shouldShow = hasValue || isFieldEditing || (editable && schema.mutability === 'READ_WRITE');
@@ -501,7 +588,7 @@ const BaseUserProfile: FC = ({
() => toggleFieldEdit(schema.name!),
)}
- {editable && schema.mutability !== 'READ_ONLY' && (
+ {editable && schema.mutability !== 'READ_ONLY' && !isReadonlyField && (