diff --git a/examples/next-rwa/src/app/member-management/page.tsx b/examples/next-rwa/src/app/member-management/page.tsx
new file mode 100644
index 000000000..9e354e616
--- /dev/null
+++ b/examples/next-rwa/src/app/member-management/page.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { OrganizationMemberManagement } from '@auth0/universal-components-react/rwa';
+
+export default function MemberManagementPage() {
+ return (
+
+ );
+}
diff --git a/examples/next-rwa/src/components/navigation/side-bar.tsx b/examples/next-rwa/src/components/navigation/side-bar.tsx
index e9a45a6f1..0eb2407d3 100644
--- a/examples/next-rwa/src/components/navigation/side-bar.tsx
+++ b/examples/next-rwa/src/components/navigation/side-bar.tsx
@@ -1,7 +1,7 @@
'use client';
import { useUser } from '@auth0/nextjs-auth0';
-import { Building, Settings, Shield, User } from 'lucide-react';
+import { Building, Settings, Shield, User, Users } from 'lucide-react';
import Link from 'next/link';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -74,6 +74,15 @@ export const Sidebar: React.FC = () => {
{t('sidebar.identity-providers')}
+
+
+
+ {t('sidebar.members')}
+
+
diff --git a/examples/next-rwa/src/providers/i18n-provider.tsx b/examples/next-rwa/src/providers/i18n-provider.tsx
index 793c0e42d..5312cc407 100644
--- a/examples/next-rwa/src/providers/i18n-provider.tsx
+++ b/examples/next-rwa/src/providers/i18n-provider.tsx
@@ -26,6 +26,7 @@ i18n.use(initReactI18next).init({
'sidebar.organization-settings': 'Organization Settings',
'sidebar.domains': 'Domains',
'sidebar.identity-providers': 'Identity Providers',
+ 'sidebar.members': 'Members',
},
},
},
diff --git a/examples/react-spa-npm/src/App.tsx b/examples/react-spa-npm/src/App.tsx
index 08ec086e1..9b443b784 100644
--- a/examples/react-spa-npm/src/App.tsx
+++ b/examples/react-spa-npm/src/App.tsx
@@ -8,6 +8,7 @@ import { Navbar } from './components/nav-bar';
import { Sidebar } from './components/side-bar';
import DomainManagementPage from './views/domain-management-page';
import HomePage from './views/home-page';
+import MemberManagementPage from './views/member-management-page';
import MFAPage from './views/mfa-page';
import OrganizationManagementPage from './views/organization-management-page';
import ProfilePage from './views/profile-page';
@@ -101,6 +102,14 @@ function AppContent() {
}
/>
+
+
+
+ }
+ />
diff --git a/examples/react-spa-npm/src/components/side-bar.tsx b/examples/react-spa-npm/src/components/side-bar.tsx
index 402398c19..58ab2150e 100644
--- a/examples/react-spa-npm/src/components/side-bar.tsx
+++ b/examples/react-spa-npm/src/components/side-bar.tsx
@@ -1,4 +1,4 @@
-import { User, Building, Settings, Shield } from 'lucide-react';
+import { User, Building, Settings, Shield, Users } from 'lucide-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@@ -70,6 +70,15 @@ export const Sidebar: React.FC = () => {
{t('sidebar.domain-management')}
+
+
+
+ {t('sidebar.member-management')}
+
+
diff --git a/examples/react-spa-npm/src/views/member-management-page.tsx b/examples/react-spa-npm/src/views/member-management-page.tsx
new file mode 100644
index 000000000..e9667f79d
--- /dev/null
+++ b/examples/react-spa-npm/src/views/member-management-page.tsx
@@ -0,0 +1,21 @@
+// import { OrganizationMemberManagement } from '@auth0/universal-components-react/spa';
+
+const MemberManagementPage = () => {
+ return (
+
+ );
+};
+
+export default MemberManagementPage;
diff --git a/examples/react-spa-shadcn/src/App.tsx b/examples/react-spa-shadcn/src/App.tsx
index c39fdede8..f8f142aa2 100644
--- a/examples/react-spa-shadcn/src/App.tsx
+++ b/examples/react-spa-shadcn/src/App.tsx
@@ -10,6 +10,7 @@ import { Sidebar } from './components/side-bar';
import { config } from './config/env';
// import { useDarkMode } from './hooks/use-dark-mode';
import DomainManagement from './pages/DomainManagement';
+import MemberManagement from './pages/MemberManagement';
import IdentityProviderManagement from './pages/IdentityProviderManagement';
import IdentityProviderManagementCreate from './pages/IdentityProviderManagementCreate';
import IdentityProviderManagementEdit from './pages/IdentityProviderManagementEdit';
@@ -136,6 +137,14 @@ const App = () => {
}
/>
+
+
+
+ }
+ />
{/* */}
diff --git a/examples/react-spa-shadcn/src/components/side-bar.tsx b/examples/react-spa-shadcn/src/components/side-bar.tsx
index e0bc08a9b..591c48d73 100644
--- a/examples/react-spa-shadcn/src/components/side-bar.tsx
+++ b/examples/react-spa-shadcn/src/components/side-bar.tsx
@@ -1,4 +1,4 @@
-import { User, Building, Shield, Settings } from 'lucide-react';
+import { User, Building, Shield, Settings, Users } from 'lucide-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@@ -70,6 +70,15 @@ export const Sidebar: React.FC = () => {
{t('sidebar.domain-management')}
+
+
+
+ {t('sidebar.member-management')}
+
+
diff --git a/examples/react-spa-shadcn/src/pages/MemberManagement.tsx b/examples/react-spa-shadcn/src/pages/MemberManagement.tsx
new file mode 100644
index 000000000..bd89bdcc3
--- /dev/null
+++ b/examples/react-spa-shadcn/src/pages/MemberManagement.tsx
@@ -0,0 +1,23 @@
+// import { OrganizationMemberManagement } from '@auth0/universal-components-react/spa';
+
+const MemberManagement = () => {
+ return (
+
+ );
+};
+
+export default MemberManagement;
diff --git a/examples/scripts/utils/env-writer.mjs b/examples/scripts/utils/env-writer.mjs
index 9b4d573fd..52eee8c88 100644
--- a/examples/scripts/utils/env-writer.mjs
+++ b/examples/scripts/utils/env-writer.mjs
@@ -72,6 +72,8 @@ const MYORG_SCOPES = [
"delete:my_org:domains",
"create:my_org:domains",
"update:my_org:domains",
+ "read:my_org:member_invitations",
+ "delete:my_org:member_invitations"
]
// My Account API scopes
diff --git a/examples/scripts/utils/resource-servers.mjs b/examples/scripts/utils/resource-servers.mjs
index d7eeb6b91..95fe0c88d 100644
--- a/examples/scripts/utils/resource-servers.mjs
+++ b/examples/scripts/utils/resource-servers.mjs
@@ -26,6 +26,8 @@ export const MYORG_API_SCOPES = [
"read:my_org:identity_providers_provisioning",
"delete:my_org:identity_providers_provisioning",
"read:my_org:configuration",
+"read:my_org:member_invitations",
+"delete:my_org:member_invitations"
]
// My Account API Scopes - desired scopes for MFA management
diff --git a/packages/react/src/components/auth0/my-organization/organization-member-management.tsx b/packages/react/src/components/auth0/my-organization/organization-member-management.tsx
new file mode 100644
index 000000000..c5bf919cd
--- /dev/null
+++ b/packages/react/src/components/auth0/my-organization/organization-member-management.tsx
@@ -0,0 +1,247 @@
+/**
+ * Organization member management component.
+ * @module organization-member-management
+ */
+
+import { getComponentStyles } from '@auth0/universal-components-core';
+import { Plus } from 'lucide-react';
+import * as React from 'react';
+
+import { GateKeeper } from '../shared/gate-keeper/gate-keeper';
+
+import { OrganizationInvitationDetailsModal } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-details/organization-invitation-details-modal';
+import { OrganizationInvitationRevokeModal } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/organization-invitation-revoke-modal';
+import { OrganizationInvitationTable } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-table/organization-invitation-table';
+import { OrganizationInvitationCreateModal } from '@/components/auth0/my-organization/shared/member-management/shared/invitation-create/organization-invitation-create-modal';
+import { Header } from '@/components/auth0/shared/header';
+import { StyledScope } from '@/components/auth0/shared/styled-scope';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { useOrganizationMemberManagement } from '@/hooks/my-organization/use-organization-member-management';
+import { useTheme } from '@/hooks/shared/use-theme';
+import { useTranslator } from '@/hooks/shared/use-translator';
+import type {
+ OrganizationMemberManagementProps,
+ UseOrganizationMemberManagementResult,
+} from '@/types/my-organization/member-management/organization-member-management-types';
+
+/**
+ * Props for the OrganizationMemberManagementView component.
+ */
+export interface OrganizationMemberManagementViewProps
+ extends UseOrganizationMemberManagementResult {
+ styling: OrganizationMemberManagementProps['styling'];
+ customMessages: OrganizationMemberManagementProps['customMessages'];
+ hideHeader: boolean;
+ readOnly: boolean;
+}
+
+/**
+ * View component for organization member management.
+ * @param props - The component props.
+ * @returns The component.
+ */
+export function OrganizationMemberManagementView(props: OrganizationMemberManagementViewProps) {
+ const {
+ styling,
+ customMessages,
+ hideHeader,
+ readOnly,
+ activeTab,
+ invitations,
+ isFetchingInvitations,
+ isCreatingInvitation,
+ isRevokingInvitation,
+ isResendingInvitation,
+ invitationPagination,
+ invitationFilters,
+ invitationSortConfig,
+ availableRoles,
+ availableProviders,
+ modalState,
+ setActiveTab,
+ openModal,
+ closeModal,
+ handleCreateSubmit,
+ handleRevokeConfirm,
+ handleRevokeResendConfirm,
+ handleCopyUrl,
+ handleSortChange,
+ handleNextPage,
+ handlePreviousPage,
+ handlePageSizeChange,
+ handleRoleFilterChange,
+ } = props;
+
+ const selectedInvitation =
+ modalState.type === 'details' ||
+ modalState.type === 'revoke' ||
+ modalState.type === 'revokeResend'
+ ? modalState.invitation
+ : null;
+
+ const { isDarkMode } = useTheme();
+ const { t } = useTranslator('member_management', customMessages as Record);
+
+ const currentStyles = React.useMemo(
+ () => getComponentStyles(styling, isDarkMode),
+ [styling, isDarkMode],
+ );
+
+ return (
+
+
+ {!hideHeader && (
+
+ openModal({ type: 'create' }),
+ icon: Plus,
+ disabled: readOnly,
+ },
+ ]
+ : []
+ }
+ />
+
+ )}
+
+
setActiveTab(value as 'members' | 'invitations')}
+ className={currentStyles.classes?.['OrganizationMemberManagement-tabs']}
+ >
+
+ {t('tabs.members')}
+ {t('tabs.invitations')}
+
+
+
+ {/* */}
+
+
+
+ openModal({ type: 'details', invitation })}
+ onCopyUrl={handleCopyUrl}
+ onRevokeAndResend={
+ readOnly
+ ? undefined
+ : (invitation) => openModal({ type: 'revokeResend', invitation })
+ }
+ onRevoke={
+ readOnly ? undefined : (invitation) => openModal({ type: 'revoke', invitation })
+ }
+ onNextPage={handleNextPage}
+ onPreviousPage={handlePreviousPage}
+ onPageSizeChange={handlePageSizeChange}
+ onRoleFilterChange={handleRoleFilterChange}
+ className={currentStyles.classes?.['OrganizationInvitationTab-table']}
+ />
+
+
+
+
+
+
invitation && openModal({ type: 'revoke', invitation })}
+ onResend={(invitation) => invitation && openModal({ type: 'revokeResend', invitation })}
+ className={currentStyles.classes?.['OrganizationInvitationTab-detailsModal']}
+ />
+
+ handleRevokeConfirm()}
+ className={currentStyles.classes?.['OrganizationInvitationTab-revokeModal']}
+ />
+
+ handleRevokeResendConfirm()}
+ className={currentStyles.classes?.['OrganizationInvitationTab-revokeResendModal']}
+ />
+
+
+ );
+}
+
+/**
+ * Container component for organization member management.
+ * @param props - The component props.
+ * @returns The component.
+ */
+export function OrganizationMemberManagement(props: OrganizationMemberManagementProps) {
+ const {
+ hideHeader = false,
+ customMessages = {},
+ styling = { variables: { common: {}, light: {}, dark: {} }, classes: {} },
+ readOnly = false,
+ createInvitationAction,
+ revokeInvitationAction,
+ resendInvitationAction,
+ } = props;
+
+ const memberManagement = useOrganizationMemberManagement({
+ customMessages,
+ readOnly,
+ createInvitationAction,
+ revokeInvitationAction,
+ resendInvitationAction,
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index 9b0ccceee..e57cc966d 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -12,6 +12,7 @@ export {
} from './auth0/my-organization/sso-provider-create';
export { SsoProviderTable, SsoProviderTableView } from './auth0/my-organization/sso-provider-table';
export { DomainTable, DomainTableView } from './auth0/my-organization/domain-table';
+export { OrganizationMemberManagement } from './auth0/my-organization/organization-member-management';
export {
OrganizationDetailsEdit,
OrganizationDetailsEditView,
diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts
index 646b4e008..f32bcfe11 100644
--- a/packages/react/src/hooks/index.ts
+++ b/packages/react/src/hooks/index.ts
@@ -28,3 +28,6 @@ export { useSsoDomainTab } from './my-organization/use-sso-domain-tab';
export { useSsoProviderCreate } from './my-organization/use-sso-provider-create';
export { useSsoProviderEdit } from './my-organization/use-sso-provider-edit';
export { useSsoProviderTable } from './my-organization/use-sso-provider-table';
+
+// Member Management hooks
+export { useOrganizationMemberManagement } from './my-organization/use-organization-member-management';
diff --git a/packages/react/src/hooks/my-organization/shared/services/use-member-management-service.ts b/packages/react/src/hooks/my-organization/shared/services/use-member-management-service.ts
new file mode 100644
index 000000000..f3a31f925
--- /dev/null
+++ b/packages/react/src/hooks/my-organization/shared/services/use-member-management-service.ts
@@ -0,0 +1,201 @@
+/**
+ * Member management service hook.
+ * @module use-member-management-service
+ * @internal
+ */
+
+import {
+ type MemberInvitation,
+ type ListIdentityProvidersResponseContent,
+} from '@auth0/universal-components-core';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import * as React from 'react';
+
+import { showToast } from '@/components/auth0/shared/toast';
+import { useCoreClient } from '@/hooks/shared/use-core-client';
+import { useTranslator } from '@/hooks/shared/use-translator';
+import type {
+ CreateInvitationInput,
+ InvitationSortConfig,
+} from '@/types/my-organization/member-management/organization-invitation-table-types';
+import type {
+ UseMemberManagementServiceOptions,
+ MemberManagementServiceResult,
+} from '@/types/my-organization/member-management/organization-member-management-types';
+
+export const memberManagementQueryKeys = {
+ all: ['member-management'] as const,
+ invitations: () => [...memberManagementQueryKeys.all, 'invitations'] as const,
+};
+
+const INVITATION_SORT_FIELD_MAP: Record = {
+ created_at: 'created_at',
+};
+
+/**
+ * Builds a sort parameter string for the API.
+ * @param sortConfig - The sort configuration.
+ * @returns The formatted sort string, or undefined if no valid sort key.
+ */
+function buildSortParam(sortConfig: InvitationSortConfig): string | undefined {
+ if (!sortConfig.key) return undefined;
+ const apiField = INVITATION_SORT_FIELD_MAP[sortConfig.key];
+ if (!apiField) return undefined;
+ const direction = sortConfig.direction === 'asc' ? '1' : '-1';
+ return `${apiField}:${direction}`;
+}
+
+/**
+ * Service hook for member management API operations.
+ * @param options - Service configuration options.
+ * @returns Query and mutation objects for member management.
+ */
+export function useMemberManagementService(
+ options: UseMemberManagementServiceOptions,
+): MemberManagementServiceResult {
+ const {
+ customMessages = {},
+ activeTab,
+ createInvitationAction,
+ revokeInvitationAction,
+ resendInvitationAction,
+ invitationParams,
+ } = options;
+
+ const isInvitationsTabActive = activeTab === 'invitations';
+
+ const { coreClient } = useCoreClient();
+ const { t } = useTranslator('member_management', customMessages as Record);
+ const queryClient = useQueryClient();
+
+ const providersQuery = useQuery({
+ queryKey: [...memberManagementQueryKeys.all, 'identity-providers'],
+ queryFn: async () => {
+ const response: ListIdentityProvidersResponseContent = await coreClient!
+ .getMyOrganizationApiClient()
+ .organization.identityProviders.list();
+ const providers = response.identity_providers ?? [];
+ return providers.map((p) => ({
+ id: p.id!,
+ name: p.display_name ?? p.name ?? '',
+ type: p.strategy,
+ }));
+ },
+ enabled: !!coreClient && isInvitationsTabActive,
+ });
+
+ const invitationsQuery = useQuery({
+ queryKey: [
+ ...memberManagementQueryKeys.invitations(),
+ invitationParams.pageSize,
+ invitationParams.fromToken,
+ invitationParams.filters,
+ invitationParams.sortConfig,
+ ],
+ queryFn: async () => {
+ const page = await coreClient!.getMyOrganizationApiClient().organization.invitations.list({
+ take: invitationParams.pageSize,
+ from: invitationParams.fromToken,
+ sort: buildSortParam(invitationParams.sortConfig),
+ });
+
+ const invitations: MemberInvitation[] = page.data;
+ const next = page.response.next ?? null;
+ const total = (page.response as Record).total as number | undefined;
+
+ return { invitations, next, total };
+ },
+ enabled: !!coreClient && isInvitationsTabActive,
+ });
+
+ const createInvitationMutation = useMutation({
+ mutationFn: async (data: CreateInvitationInput) => {
+ if (createInvitationAction?.onBefore && !createInvitationAction.onBefore(data)) {
+ throw new Error('Create action cancelled by onBefore');
+ }
+ const response = await coreClient!
+ .getMyOrganizationApiClient()
+ .organization.invitations.create({
+ invitees: data.invitees,
+ inviter: data.inviter,
+ ttl_sec: data.ttl_sec,
+ });
+ return Array.isArray(response) ? response[0] : response;
+ },
+ onSuccess: (result, data) => {
+ createInvitationAction?.onAfter?.(data, result);
+ showToast({ type: 'success', message: t('invitation.create.success') });
+ queryClient.invalidateQueries({ queryKey: memberManagementQueryKeys.invitations() });
+ },
+ onError: () => {
+ showToast({ type: 'error', message: t('invitation.error.create_failed') });
+ },
+ });
+
+ const revokeInvitationMutation = useMutation({
+ mutationFn: async (invitation: MemberInvitation) => {
+ if (revokeInvitationAction?.onBefore && !revokeInvitationAction.onBefore(invitation)) {
+ throw new Error('Revoke action cancelled by onBefore');
+ }
+ await coreClient!
+ .getMyOrganizationApiClient()
+ .organization.invitations.delete(invitation.id!);
+ return invitation;
+ },
+ onSuccess: (invitation) => {
+ revokeInvitationAction?.onAfter?.(invitation);
+ showToast({ type: 'success', message: t('invitation.revoke.success') });
+ queryClient.invalidateQueries({ queryKey: memberManagementQueryKeys.invitations() });
+ },
+ onError: () => {
+ showToast({ type: 'error', message: t('invitation.error.revoke_failed') });
+ },
+ });
+
+ const resendInvitationMutation = useMutation({
+ mutationFn: async (invitation: MemberInvitation) => {
+ if (resendInvitationAction?.onBefore && !resendInvitationAction.onBefore(invitation)) {
+ throw new Error('Resend action cancelled by onBefore');
+ }
+ const freshInvitation = await coreClient!
+ .getMyOrganizationApiClient()
+ .organization.invitations.get(invitation.id!);
+ await coreClient!
+ .getMyOrganizationApiClient()
+ .organization.invitations.delete(freshInvitation.id ?? invitation.id!);
+ const email = freshInvitation.invitee?.email ?? invitation.invitee?.email ?? '';
+ const roles = freshInvitation.roles ?? invitation.roles;
+ const response = await coreClient!
+ .getMyOrganizationApiClient()
+ .organization.invitations.create({
+ invitees: [{ email, roles }],
+ });
+ return Array.isArray(response) ? response[0] : response;
+ },
+ onSuccess: (result, invitation) => {
+ resendInvitationAction?.onAfter?.(invitation, result);
+ showToast({ type: 'success', message: t('invitation.success.invitation_resent') });
+ queryClient.invalidateQueries({ queryKey: memberManagementQueryKeys.invitations() });
+ },
+ onError: () => {
+ showToast({ type: 'error', message: t('invitation.error.resend_failed') });
+ queryClient.invalidateQueries({ queryKey: memberManagementQueryKeys.invitations() });
+ },
+ });
+
+ const fetchInvitationDetails = React.useCallback(
+ async (invitationId: string): Promise => {
+ return coreClient!.getMyOrganizationApiClient().organization.invitations.get(invitationId);
+ },
+ [coreClient],
+ );
+
+ return {
+ providersQuery,
+ invitationsQuery,
+ createInvitationMutation,
+ revokeInvitationMutation,
+ resendInvitationMutation,
+ fetchInvitationDetails,
+ };
+}
diff --git a/packages/react/src/hooks/my-organization/use-config.ts b/packages/react/src/hooks/my-organization/use-config.ts
index 01c57aa0e..bf0488256 100644
--- a/packages/react/src/hooks/my-organization/use-config.ts
+++ b/packages/react/src/hooks/my-organization/use-config.ts
@@ -51,6 +51,11 @@ export function useConfig(): UseConfigResult {
const isConfigValid = !!allowedStrategies?.length;
+ const allowedRoles =
+ ((config as Record)?.allowed_roles as
+ | Array<{ id: string; name: string; description?: string }>
+ | undefined) ?? [];
+
return {
config: config ?? null,
isLoadingConfig: configQuery.isLoading,
@@ -58,6 +63,6 @@ export function useConfig(): UseConfigResult {
filteredStrategies,
shouldAllowDeletion,
isConfigValid,
- allowedRoles: [],
+ allowedRoles,
};
}
diff --git a/packages/react/src/hooks/my-organization/use-organization-member-management.ts b/packages/react/src/hooks/my-organization/use-organization-member-management.ts
new file mode 100644
index 000000000..e7c5b232e
--- /dev/null
+++ b/packages/react/src/hooks/my-organization/use-organization-member-management.ts
@@ -0,0 +1,225 @@
+/**
+ * Organization member management hook.
+ * @module use-organization-member-management
+ */
+
+import { type MemberInvitation } from '@auth0/universal-components-core';
+import * as React from 'react';
+
+import { showToast } from '@/components/auth0/shared/toast';
+import { useMemberManagementService } from '@/hooks/my-organization/shared/services/use-member-management-service';
+import { useConfig } from '@/hooks/my-organization/use-config';
+import { useCheckpointPagination } from '@/hooks/shared/use-checkpoint-pagination';
+import { useTranslator } from '@/hooks/shared/use-translator';
+import type {
+ CreateInvitationInput,
+ InvitationFilterState,
+ InvitationSortConfig,
+ RoleOption,
+ IdentityProviderOption,
+} from '@/types/my-organization/member-management/organization-invitation-table-types';
+import type {
+ ActiveTab,
+ MemberManagementModalState,
+ UseOrganizationMemberManagementOptions,
+ UseOrganizationMemberManagementResult,
+} from '@/types/my-organization/member-management/organization-member-management-types';
+
+export { memberManagementQueryKeys } from '@/hooks/my-organization/shared/services/use-member-management-service';
+
+/**
+ * Hook for organization member management.
+ * @param options - Hook configuration options.
+ * @returns State and handler functions.
+ */
+export function useOrganizationMemberManagement(
+ options: UseOrganizationMemberManagementOptions,
+): UseOrganizationMemberManagementResult {
+ const {
+ customMessages = {},
+ readOnly = false,
+ createInvitationAction,
+ revokeInvitationAction,
+ resendInvitationAction,
+ } = options;
+
+ const { t } = useTranslator('member_management', customMessages as Record);
+
+ const [activeTab, setActiveTab] = React.useState('members');
+
+ const { allowedRoles } = useConfig();
+ const availableRoles: RoleOption[] = allowedRoles;
+
+ const {
+ pageSize: invitationPageSize,
+ currentPage: invitationCurrentPage,
+ fromToken: invitationFromToken,
+ hasPreviousPage: invitationHasPreviousPage,
+ sortConfig: invitationSortConfig,
+ filters: invitationFilters,
+ goToNextPage: invitationGoToNextPage,
+ goToPreviousPage: invitationGoToPreviousPage,
+ changePageSize: invitationChangePageSize,
+ changeSortConfig: invitationChangeSortConfig,
+ changeFilters: invitationChangeFilters,
+ } = useCheckpointPagination();
+
+ const [modalState, setModalState] = React.useState({ type: null });
+ const detailsRequestIdRef = React.useRef(0);
+
+ const {
+ providersQuery,
+ invitationsQuery,
+ createInvitationMutation,
+ revokeInvitationMutation,
+ resendInvitationMutation,
+ fetchInvitationDetails,
+ } = useMemberManagementService({
+ customMessages,
+ activeTab,
+ createInvitationAction,
+ revokeInvitationAction,
+ resendInvitationAction,
+ invitationParams: {
+ pageSize: invitationPageSize,
+ fromToken: invitationFromToken,
+ sortConfig: invitationSortConfig,
+ filters: invitationFilters,
+ },
+ });
+
+ const availableProviders: IdentityProviderOption[] = providersQuery.data ?? [];
+ const currentInvitations = invitationsQuery.data?.invitations ?? [];
+ const invitationNextToken = invitationsQuery.data?.next ?? null;
+ const invitationsTotalItems = invitationsQuery.data?.total;
+
+ const openModal = React.useCallback(
+ async (state: MemberManagementModalState) => {
+ if (state.type === 'create' && readOnly) return;
+ if ((state.type === 'revoke' || state.type === 'revokeResend') && readOnly) return;
+ setModalState(state);
+
+ if (state.type === 'details') {
+ const requestId = ++detailsRequestIdRef.current;
+ try {
+ const response = await fetchInvitationDetails(state.invitation.id!);
+ if (detailsRequestIdRef.current === requestId) {
+ setModalState({ type: 'details', invitation: response });
+ }
+ } catch {
+ if (detailsRequestIdRef.current === requestId) {
+ showToast({ type: 'error', message: t('invitation.error.fetch_failed') });
+ }
+ }
+ }
+ },
+ [readOnly, fetchInvitationDetails, t],
+ );
+
+ const closeModal = React.useCallback(() => {
+ setModalState({ type: null });
+ }, []);
+
+ const handleCreateSubmit = React.useCallback(
+ (data: CreateInvitationInput) => {
+ createInvitationMutation.mutate(data, {
+ onSuccess: () => closeModal(),
+ });
+ },
+ [createInvitationMutation, closeModal],
+ );
+
+ const handleRevokeConfirm = React.useCallback(() => {
+ if (modalState.type !== 'revoke') return;
+ revokeInvitationMutation.mutate(modalState.invitation, {
+ onSuccess: () => closeModal(),
+ });
+ }, [modalState, revokeInvitationMutation, closeModal]);
+
+ const handleRevokeResendConfirm = React.useCallback(() => {
+ if (modalState.type !== 'revokeResend') return;
+ resendInvitationMutation.mutate(modalState.invitation, {
+ onSuccess: () => closeModal(),
+ });
+ }, [modalState, resendInvitationMutation, closeModal]);
+
+ const handleCopyUrl = React.useCallback(
+ async (invitation: MemberInvitation) => {
+ if (!invitation.invitation_url) return;
+ try {
+ await navigator.clipboard.writeText(invitation.invitation_url);
+ showToast({ type: 'success', message: t('invitation.success.url_copied') });
+ } catch {
+ showToast({ type: 'error', message: t('invitation.error.copy_url_failed') });
+ }
+ },
+ [t],
+ );
+
+ const handleNextPage = React.useCallback(() => {
+ if (invitationNextToken) {
+ invitationGoToNextPage(invitationNextToken);
+ }
+ }, [invitationNextToken, invitationGoToNextPage]);
+
+ const handlePreviousPage = React.useCallback(() => {
+ invitationGoToPreviousPage();
+ }, [invitationGoToPreviousPage]);
+
+ const handlePageSizeChange = React.useCallback(
+ (pageSize: number) => {
+ invitationChangePageSize(pageSize);
+ },
+ [invitationChangePageSize],
+ );
+
+ const handleSortChange = React.useCallback(
+ (sortConfig: InvitationSortConfig) => {
+ invitationChangeSortConfig(sortConfig);
+ },
+ [invitationChangeSortConfig],
+ );
+
+ const handleRoleFilterChange = React.useCallback(
+ (roleId: string | undefined) => {
+ invitationChangeFilters((prev) => ({ ...prev, roleId }));
+ },
+ [invitationChangeFilters],
+ );
+
+ return {
+ activeTab,
+ isLoading: invitationsQuery.isLoading || invitationsQuery.isFetching,
+ availableRoles,
+ availableProviders,
+
+ invitations: currentInvitations,
+ isFetchingInvitations: invitationsQuery.isLoading || invitationsQuery.isFetching,
+ isCreatingInvitation: createInvitationMutation.isPending,
+ isRevokingInvitation: revokeInvitationMutation.isPending,
+ isResendingInvitation: resendInvitationMutation.isPending,
+ invitationPagination: {
+ pageSize: invitationPageSize,
+ currentPage: invitationCurrentPage,
+ totalItems: invitationsTotalItems,
+ hasNextPage: !!invitationNextToken,
+ hasPreviousPage: invitationHasPreviousPage,
+ },
+ invitationFilters,
+ invitationSortConfig,
+ modalState,
+
+ setActiveTab,
+ openModal,
+ closeModal,
+ handleCreateSubmit,
+ handleRevokeConfirm,
+ handleRevokeResendConfirm,
+ handleCopyUrl,
+ handleNextPage,
+ handlePreviousPage,
+ handlePageSizeChange,
+ handleSortChange,
+ handleRoleFilterChange,
+ };
+}