diff --git a/packages/core/src/i18n/custom-messages/my-organization/index.ts b/packages/core/src/i18n/custom-messages/my-organization/index.ts
index 27bc62393..845ecef45 100644
--- a/packages/core/src/i18n/custom-messages/my-organization/index.ts
+++ b/packages/core/src/i18n/custom-messages/my-organization/index.ts
@@ -7,3 +7,5 @@
export * from './idp-management';
export * from './organization-management';
export * from './domain-management';
+export * from './member-management/invitation-tab-types';
+export * from './member-management/member-management-types';
diff --git a/packages/core/src/i18n/custom-messages/my-organization/member-management/invitation-tab-types.ts b/packages/core/src/i18n/custom-messages/my-organization/member-management/invitation-tab-types.ts
new file mode 100644
index 000000000..e51228669
--- /dev/null
+++ b/packages/core/src/i18n/custom-messages/my-organization/member-management/invitation-tab-types.ts
@@ -0,0 +1,90 @@
+/**
+ * Custom message type definitions for invitation tab.
+ * @module invitation-tab-types
+ * @internal
+ */
+
+export interface OrganizationInvitationTabMessages {
+ table?: {
+ columns?: {
+ email?: string;
+ status?: string;
+ inviter?: string;
+ created_at?: string;
+ expires_at?: string;
+ roles?: string;
+ };
+ empty_message?: string;
+ search_placeholder?: string;
+ filter_by_role?: string;
+ all_roles?: string;
+ reset_filter?: string;
+ showing_results?: string;
+ status_pending?: string;
+ status_expired?: string;
+ };
+ actions?: {
+ menu_label?: string;
+ view_details?: string;
+ copy_url?: string;
+ revoke_and_resend?: string;
+ revoke?: string;
+ };
+ create?: {
+ title?: string;
+ description?: string;
+ email_label?: string;
+ email_placeholder?: string;
+ email_helper?: string;
+ email_limit_error?: string;
+ email_invalid_error?: string;
+ email_duplicate_error?: string;
+ email_required_error?: string;
+ roles_label?: string;
+ roles_placeholder?: string;
+ provider_label?: string;
+ provider_placeholder?: string;
+ submit_button?: string;
+ creating?: string;
+ cancel_button?: string;
+ };
+ details?: {
+ title?: string;
+ email_label?: string;
+ status_label?: string;
+ roles_label?: string;
+ provider_label?: string;
+ created_at_label?: string;
+ expires_at_label?: string;
+ invited_by_label?: string;
+ invitation_url_label?: string;
+ copy_url_button?: string;
+ close_button?: string;
+ revoke_button?: string;
+ resend_button?: string;
+ };
+ revoke?: {
+ title?: string;
+ description?: string;
+ confirm_button?: string;
+ cancel_button?: string;
+ };
+ revoke_resend?: {
+ title?: string;
+ description?: string;
+ confirm_button?: string;
+ cancel_button?: string;
+ };
+ success?: {
+ url_copied?: string;
+ invitation_resent?: string;
+ };
+ error?: {
+ fetch_failed?: string;
+ create_failed?: string;
+ revoke_failed?: string;
+ resend_failed?: string;
+ revoke_resend_failed?: string;
+ copy_url_failed?: string;
+ };
+}
diff --git a/packages/core/src/i18n/custom-messages/my-organization/member-management/member-management-types.ts b/packages/core/src/i18n/custom-messages/my-organization/member-management/member-management-types.ts
new file mode 100644
index 000000000..60bfc9796
--- /dev/null
+++ b/packages/core/src/i18n/custom-messages/my-organization/member-management/member-management-types.ts
@@ -0,0 +1,19 @@
+/**
+ * Custom message type definitions for member management.
+ * @module member-management-types
+ * @internal
+ */
+
+import type { OrganizationInvitationTabMessages } from './invitation-tab-types';
+
+export interface OrganizationMemberManagementMessages {
+ header?: {
+ title?: string;
+ description?: string;
+ };
+ tabs?: {
+ members?: string;
+ invitations?: string;
+ };
+ invitation?: OrganizationInvitationTabMessages;
+}
diff --git a/packages/core/src/i18n/translations/en-US.json b/packages/core/src/i18n/translations/en-US.json
index 2cd7c8218..2cfc2478d 100644
--- a/packages/core/src/i18n/translations/en-US.json
+++ b/packages/core/src/i18n/translations/en-US.json
@@ -1069,5 +1069,124 @@
"install_guardian_description": "In order to continue, install the Auth0 Guardian app via the app store from your mobile device"
}
}
+ },
+ "member_management": {
+ "header": {
+ "title": "Members",
+ "description": "Manage the members of your organization."
+ },
+ "invite_button": "Invite Member",
+ "tabs": {
+ "members": "Members",
+ "invitations": "Invitations"
+ },
+ "member": {
+ "table": {
+ "columns": {
+ "name": "Name",
+ "email": "Email",
+ "roles": "Roles"
+ },
+ "empty_message": "No members found."
+ },
+ "remove": {
+ "title": "Remove Member",
+ "description": "Are you sure you want to remove ${name} from this organization?",
+ "confirm_button": "Remove",
+ "cancel_button": "Cancel",
+ "success": "${name} has been removed from the organization."
+ },
+ "error": {
+ "fetch_failed": "Failed to load members. Please try again.",
+ "remove_failed": "Failed to remove member. Please try again."
+ }
+ },
+ "invitation": {
+ "table": {
+ "columns": {
+ "email": "Email",
+ "status": "Status",
+ "inviter": "Invited By",
+ "created_at": "Created At",
+ "expires_at": "Expires At",
+ "roles": "Roles"
+ },
+ "empty_message": "No pending invitations.",
+ "search_placeholder": "Search by email...",
+ "filter_by_role": "Filter By Role",
+ "all_roles": "All",
+ "reset_filter": "Reset",
+ "showing_results": "Showing ${start}-${end} of ${total}",
+ "status_pending": "Pending",
+ "status_expired": "Expired"
+ },
+ "actions": {
+ "menu_label": "Actions",
+ "view_details": "View Details",
+ "copy_url": "Copy Invitation URL",
+ "revoke_and_resend": "Revoke and Resend",
+ "revoke": "Revoke Invitation"
+ },
+ "create": {
+ "title": "Invite Member",
+ "description": "Send an invitation to join this organization.",
+ "email_label": "Email Address",
+ "email_placeholder": "Enter email address and press Enter",
+ "email_helper": "Add up to 10 members in a comma-separated list.",
+ "email_limit_error": "You can add up to 10 email addresses.",
+ "email_invalid_error": "Please enter a valid email address.",
+ "email_duplicate_error": "This email has already been added.",
+ "email_required_error": "Please enter at least one email address.",
+ "roles_label": "Roles",
+ "roles_placeholder": "Select roles (optional)",
+ "provider_label": "Provider",
+ "provider_placeholder": "Select provider (optional)",
+ "provider_helper": "If none is selected, the member can log in with any provider.",
+ "submit_button": "Send Invite",
+ "creating": "Creating...",
+ "cancel_button": "Cancel",
+ "success": "Invitation sent to ${email}."
+ },
+ "details": {
+ "title": "Invitation Details",
+ "email_label": "Email",
+ "status_label": "Status",
+ "roles_label": "Roles",
+ "provider_label": "Identity Provider",
+ "created_at_label": "Created",
+ "expires_at_label": "Expires",
+ "invited_by_label": "Invited By",
+ "invitation_url_label": "Invitation URL",
+ "copy_url_button": "Copy",
+ "close_button": "Close",
+ "revoke_button": "Revoke Invitation",
+ "resend_button": "Revoke and Resend"
+ },
+ "revoke": {
+ "title": "Revoke Invitation",
+ "description": "Are you sure you want to revoke the invitation to ${email}?",
+ "confirm_button": "Revoke",
+ "cancel_button": "Cancel",
+ "success": "Invitation for ${email} has been revoked."
+ },
+ "revoke_resend": {
+ "title": "Revoke and Resend Invitation",
+ "description": "Are you sure you want to revoke the current invitation and send a new one to ${email}?",
+ "confirm_button": "Proceed",
+ "cancel_button": "Cancel"
+ },
+ "success": {
+ "url_copied": "Invitation URL copied to clipboard.",
+ "invitation_resent": "Invitation resent to ${email}."
+ },
+ "error": {
+ "fetch_failed": "Failed to load invitations. Please try again.",
+ "create_failed": "Failed to send invitation. Please try again.",
+ "revoke_failed": "Failed to revoke invitation. Please try again.",
+ "resend_failed": "Failed to resend invitation. Please try again.",
+ "revoke_resend_failed": "Failed to revoke and resend invitation. Please try again.",
+ "copy_url_failed": "Failed to copy invitation URL. Please try again."
+ }
+ }
}
}
diff --git a/packages/core/src/i18n/translations/ja.json b/packages/core/src/i18n/translations/ja.json
index 73bc5d62b..a877c73a4 100644
--- a/packages/core/src/i18n/translations/ja.json
+++ b/packages/core/src/i18n/translations/ja.json
@@ -1071,5 +1071,124 @@
"install_guardian_description": "続行するには、お使いのモバイルデバイスからアプリストア経由でAuth0 Guardianアプリをインストールしてください。"
}
}
+ },
+ "member_management": {
+ "header": {
+ "title": "メンバー",
+ "description": "組織のメンバーを管理します。"
+ },
+ "invite_button": "メンバーを招待",
+ "tabs": {
+ "members": "メンバー",
+ "invitations": "招待"
+ },
+ "member": {
+ "table": {
+ "columns": {
+ "name": "名前",
+ "email": "メール",
+ "roles": "ロール"
+ },
+ "empty_message": "メンバーが見つかりません。"
+ },
+ "remove": {
+ "title": "メンバーを削除",
+ "description": "この組織から${name}を削除してもよろしいですか?",
+ "confirm_button": "削除",
+ "cancel_button": "キャンセル",
+ "success": "${name}が組織から削除されました。"
+ },
+ "error": {
+ "fetch_failed": "メンバーの読み込みに失敗しました。もう一度お試しください。",
+ "remove_failed": "メンバーの削除に失敗しました。もう一度お試しください。"
+ }
+ },
+ "invitation": {
+ "table": {
+ "columns": {
+ "email": "メール",
+ "status": "ステータス",
+ "inviter": "招待者",
+ "created_at": "作成日",
+ "expires_at": "有効期限",
+ "roles": "ロール"
+ },
+ "empty_message": "保留中の招待はありません。",
+ "search_placeholder": "メールで検索...",
+ "filter_by_role": "ロールで絞り込み",
+ "all_roles": "すべて",
+ "reset_filter": "リセット",
+ "showing_results": "${start}〜${end} / ${total}件",
+ "status_pending": "保留中",
+ "status_expired": "期限切れ"
+ },
+ "actions": {
+ "menu_label": "アクション",
+ "view_details": "詳細を表示",
+ "copy_url": "招待URLをコピー",
+ "revoke_and_resend": "取り消して再送信",
+ "revoke": "招待を取り消す"
+ },
+ "create": {
+ "title": "メンバーを招待",
+ "description": "この組織に参加するための招待を送信します。",
+ "email_label": "メールアドレス",
+ "email_placeholder": "メールアドレスを入力してEnterを押してください",
+ "email_helper": "カンマ区切りで最大10名まで追加できます。",
+ "email_limit_error": "メールアドレスは最大10件まで追加できます。",
+ "email_invalid_error": "有効なメールアドレスを入力してください。",
+ "email_duplicate_error": "このメールアドレスは既に追加されています。",
+ "email_required_error": "メールアドレスを1つ以上入力してください。",
+ "roles_label": "ロール",
+ "roles_placeholder": "ロールを選択(任意)",
+ "provider_label": "プロバイダー",
+ "provider_placeholder": "プロバイダーを選択(任意)",
+ "provider_helper": "選択しない場合、メンバーは任意のプロバイダーでログインできます。",
+ "submit_button": "招待を送信",
+ "creating": "作成中...",
+ "cancel_button": "キャンセル",
+ "success": "${email}に招待を送信しました。"
+ },
+ "details": {
+ "title": "招待の詳細",
+ "email_label": "メール",
+ "status_label": "ステータス",
+ "roles_label": "ロール",
+ "provider_label": "IDプロバイダー",
+ "created_at_label": "作成日",
+ "expires_at_label": "有効期限",
+ "invited_by_label": "招待者",
+ "invitation_url_label": "招待URL",
+ "copy_url_button": "コピー",
+ "close_button": "閉じる",
+ "revoke_button": "招待を取り消す",
+ "resend_button": "取り消して再送信"
+ },
+ "revoke": {
+ "title": "招待を取り消す",
+ "description": "${email}への招待を取り消してもよろしいですか?",
+ "confirm_button": "取り消す",
+ "cancel_button": "キャンセル",
+ "success": "${email}への招待が取り消されました。"
+ },
+ "revoke_resend": {
+ "title": "招待を取り消して再送信",
+ "description": "現在の招待を取り消し、${email}に新しい招待を送信してもよろしいですか?",
+ "confirm_button": "続行",
+ "cancel_button": "キャンセル"
+ },
+ "success": {
+ "url_copied": "招待URLをクリップボードにコピーしました。",
+ "invitation_resent": "${email}に招待を再送信しました。"
+ },
+ "error": {
+ "fetch_failed": "招待の読み込みに失敗しました。もう一度お試しください。",
+ "create_failed": "招待の送信に失敗しました。もう一度お試しください。",
+ "revoke_failed": "招待の取り消しに失敗しました。もう一度お試しください。",
+ "resend_failed": "招待の再送信に失敗しました。もう一度お試しください。",
+ "revoke_resend_failed": "招待の取り消しと再送信に失敗しました。もう一度お試しください。",
+ "copy_url_failed": "招待URLのコピーに失敗しました。もう一度お試しください。"
+ }
+ }
}
}
diff --git a/packages/core/src/schemas/my-organization/index.ts b/packages/core/src/schemas/my-organization/index.ts
index 28b4269df..c800eec7e 100644
--- a/packages/core/src/schemas/my-organization/index.ts
+++ b/packages/core/src/schemas/my-organization/index.ts
@@ -7,3 +7,6 @@
export * from './organization-management';
export * from './idp-management';
export * from './domain-management';
+export * from './member-management/invitations/invitation-schema';
+export * from './member-management/invitations/invitation-create-schema';
+export * from './member-management/invitations/invitation-create-schema-types';
diff --git a/packages/core/src/schemas/my-organization/member-management/invitations/__tests__/invitation-create-schema.test.ts b/packages/core/src/schemas/my-organization/member-management/invitations/__tests__/invitation-create-schema.test.ts
new file mode 100644
index 000000000..e24e90ec0
--- /dev/null
+++ b/packages/core/src/schemas/my-organization/member-management/invitations/__tests__/invitation-create-schema.test.ts
@@ -0,0 +1,226 @@
+import { describe, it, expect } from 'vitest';
+
+import {
+ createInvitationCreateSchema,
+ invitationCreateSchemaDefaults,
+} from '../invitation-create-schema';
+
+describe('Invitation Create Schema', () => {
+ describe('default schema', () => {
+ describe.each([
+ { input: 'user@example.com', shouldPass: true, description: 'simple email' },
+ { input: 'test.user@domain.com', shouldPass: true, description: 'email with dots' },
+ { input: 'user+tag@example.com', shouldPass: true, description: 'email with plus tag' },
+ { input: 'user@sub.domain.com', shouldPass: true, description: 'email with subdomain' },
+ { input: 'a@b.co', shouldPass: true, description: 'minimal valid email' },
+ {
+ input: 'user-name@example.org',
+ shouldPass: true,
+ description: 'email with hyphen in local',
+ },
+ {
+ input: 'user_name@example.org',
+ shouldPass: true,
+ description: 'email with underscore in local',
+ },
+ ])('when email is "$input" ($description)', ({ input, shouldPass }) => {
+ it(`should ${shouldPass ? 'accept' : 'reject'}`, () => {
+ const result = invitationCreateSchemaDefaults.emailSchema.safeParse(input);
+ expect(result.success).toBe(shouldPass);
+ });
+ });
+
+ describe.each([
+ { input: '', shouldPass: false, description: 'empty string' },
+ { input: ' ', shouldPass: false, description: 'whitespace only' },
+ { input: 'notanemail', shouldPass: false, description: 'missing @ and domain' },
+ { input: '@example.com', shouldPass: false, description: 'missing local part' },
+ { input: 'user@', shouldPass: false, description: 'missing domain' },
+ { input: 'user@.com', shouldPass: false, description: 'missing domain name' },
+ { input: 'user @example.com', shouldPass: false, description: 'space in local part' },
+ ])('when email is "$input" ($description)', ({ input, shouldPass }) => {
+ it(`should ${shouldPass ? 'accept' : 'reject'}`, () => {
+ const result = invitationCreateSchemaDefaults.emailSchema.safeParse(input);
+ expect(result.success).toBe(shouldPass);
+ });
+ });
+
+ it('should return the default error message on validation failure', () => {
+ const result = invitationCreateSchemaDefaults.emailSchema.safeParse('');
+ expect(result.success).toBe(false);
+ if (!result.success && result.error?.errors[0]) {
+ expect(result.error.errors[0].message).toBe('Please enter a valid email address');
+ }
+ });
+
+ it('should have default maxEmails of 10', () => {
+ expect(invitationCreateSchemaDefaults.maxEmails).toBe(10);
+ });
+
+ it('should have default emailRegex', () => {
+ expect(invitationCreateSchemaDefaults.emailRegex).toBeInstanceOf(RegExp);
+ expect(invitationCreateSchemaDefaults.emailRegex.test('user@example.com')).toBe(true);
+ });
+ });
+
+ describe('createInvitationCreateSchema factory', () => {
+ describe('with custom error message', () => {
+ it('should use custom default error message', () => {
+ const customMessage = 'Custom email validation error';
+ const schema = createInvitationCreateSchema({}, customMessage);
+ const result = schema.emailSchema.safeParse('');
+
+ expect(result.success).toBe(false);
+ if (!result.success && result.error?.errors[0]) {
+ expect(result.error.errors[0].message).toBe(customMessage);
+ }
+ });
+
+ it('should use field-level error message over default', () => {
+ const fieldError = 'Field-level error';
+ const defaultError = 'Default error';
+ const schema = createInvitationCreateSchema(
+ { email: { errorMessage: fieldError } },
+ defaultError,
+ );
+ const result = schema.emailSchema.safeParse('invalid');
+
+ expect(result.success).toBe(false);
+ if (!result.success && result.error?.errors[0]) {
+ expect(result.error.errors[0].message).toBe(fieldError);
+ }
+ });
+ });
+
+ describe('with custom regex', () => {
+ it('should accept emails matching the custom regex', () => {
+ const customRegex = /^[\w.+-]+@auth0\.com$/;
+ const schema = createInvitationCreateSchema({
+ email: {
+ regex: customRegex,
+ errorMessage: 'Only @auth0.com emails allowed',
+ },
+ });
+
+ expect(schema.emailSchema.safeParse('user@auth0.com').success).toBe(true);
+ expect(schema.emailSchema.safeParse('test.user@auth0.com').success).toBe(true);
+ });
+
+ it('should reject emails not matching the custom regex', () => {
+ const customRegex = /^[\w.+-]+@auth0\.com$/;
+ const schema = createInvitationCreateSchema({
+ email: {
+ regex: customRegex,
+ errorMessage: 'Only @auth0.com emails allowed',
+ },
+ });
+
+ expect(schema.emailSchema.safeParse('user@example.com').success).toBe(false);
+ expect(schema.emailSchema.safeParse('user@gmail.com').success).toBe(false);
+ });
+
+ it('should use custom error message with custom regex validation failure', () => {
+ const customRegex = /^[\w.+-]+@auth0\.com$/;
+ const customErrorMessage = 'Only @auth0.com emails allowed';
+ const schema = createInvitationCreateSchema({
+ email: {
+ regex: customRegex,
+ errorMessage: customErrorMessage,
+ },
+ });
+ const result = schema.emailSchema.safeParse('user@example.com');
+
+ expect(result.success).toBe(false);
+ if (!result.success && result.error?.errors[0]) {
+ expect(result.error.errors[0].message).toBe(customErrorMessage);
+ }
+ });
+ });
+
+ describe('with custom maxEmails', () => {
+ it('should override the default maxEmails', () => {
+ const schema = createInvitationCreateSchema({ maxEmails: 5 });
+ expect(schema.maxEmails).toBe(5);
+ });
+
+ it('should allow maxEmails of 1', () => {
+ const schema = createInvitationCreateSchema({ maxEmails: 1 });
+ expect(schema.maxEmails).toBe(1);
+ });
+
+ it('should default to 10 when maxEmails is not provided', () => {
+ const schema = createInvitationCreateSchema({});
+ expect(schema.maxEmails).toBe(10);
+ });
+ });
+
+ describe('with empty options', () => {
+ it('should behave like the default schema', () => {
+ const schema = createInvitationCreateSchema({});
+
+ expect(schema.emailSchema.safeParse('user@example.com').success).toBe(true);
+ expect(schema.emailSchema.safeParse('').success).toBe(false);
+ expect(schema.maxEmails).toBe(10);
+ });
+ });
+
+ describe('with undefined options', () => {
+ it('should behave like the default schema', () => {
+ const schema = createInvitationCreateSchema(undefined);
+
+ expect(schema.emailSchema.safeParse('user@example.com').success).toBe(true);
+ expect(schema.emailSchema.safeParse('').success).toBe(false);
+ expect(schema.maxEmails).toBe(10);
+ });
+ });
+
+ describe('with no arguments', () => {
+ it('should return defaults', () => {
+ const schema = createInvitationCreateSchema();
+
+ expect(schema.emailSchema.safeParse('user@example.com').success).toBe(true);
+ expect(schema.emailSchema.safeParse('').success).toBe(false);
+ expect(schema.maxEmails).toBe(10);
+ expect(schema.emailErrorMessage).toBe('Please enter a valid email address');
+ });
+ });
+ });
+
+ describe('return value structure', () => {
+ it('should return emailSchema, emailRegex, emailErrorMessage, and maxEmails', () => {
+ const result = createInvitationCreateSchema();
+
+ expect(result).toHaveProperty('emailSchema');
+ expect(result).toHaveProperty('emailRegex');
+ expect(result).toHaveProperty('emailErrorMessage');
+ expect(result).toHaveProperty('maxEmails');
+ });
+
+ it('should return a Zod schema for emailSchema', () => {
+ const result = createInvitationCreateSchema();
+ expect(result.emailSchema.safeParse).toBeDefined();
+ expect(typeof result.emailSchema.safeParse).toBe('function');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle email field config with only regex (no errorMessage)', () => {
+ const schema = createInvitationCreateSchema({
+ email: { regex: /^[\w.+-]+@company\.com$/ },
+ });
+
+ // Should use the default error message since none was provided
+ expect(schema.emailErrorMessage).toBe('Please enter a valid email address');
+ });
+
+ it('should handle email field config with only errorMessage (no regex)', () => {
+ const schema = createInvitationCreateSchema({
+ email: { errorMessage: 'Custom error' },
+ });
+
+ // Should still use the default regex
+ expect(schema.emailSchema.safeParse('user@example.com').success).toBe(true);
+ expect(schema.emailErrorMessage).toBe('Custom error');
+ });
+ });
+});
diff --git a/packages/core/src/schemas/my-organization/member-management/invitations/invitation-create-schema-types.ts b/packages/core/src/schemas/my-organization/member-management/invitations/invitation-create-schema-types.ts
new file mode 100644
index 000000000..b115aa96d
--- /dev/null
+++ b/packages/core/src/schemas/my-organization/member-management/invitations/invitation-create-schema-types.ts
@@ -0,0 +1,24 @@
+/**
+ * Invitation creation schema type definitions.
+ * @module invitation-create-schema-types
+ * @internal
+ */
+
+/**
+ * Schema configuration for a single email field.
+ * @internal
+ */
+export interface EmailFieldConfig {
+ regex?: RegExp;
+ errorMessage?: string;
+}
+
+/**
+ * Schema configuration for invitation creation form.
+ * Consumers can override validation rules for each field.
+ * @internal
+ */
+export interface InvitationCreateSchemas {
+ email?: EmailFieldConfig;
+ maxEmails?: number;
+}
diff --git a/packages/core/src/schemas/my-organization/member-management/invitations/invitation-create-schema.ts b/packages/core/src/schemas/my-organization/member-management/invitations/invitation-create-schema.ts
new file mode 100644
index 000000000..557818a4e
--- /dev/null
+++ b/packages/core/src/schemas/my-organization/member-management/invitations/invitation-create-schema.ts
@@ -0,0 +1,77 @@
+/**
+ * Invitation creation schema for form validation.
+ * @module invitation-create-schema
+ * @internal
+ */
+
+import { z } from 'zod';
+
+import { type InvitationCreateSchemas } from './invitation-create-schema-types';
+
+/** Default email regex pattern. */
+const DEFAULT_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+/** Default maximum number of email addresses allowed. */
+const DEFAULT_MAX_EMAILS = 10;
+
+/**
+ * Helper to merge schema field config with defaults.
+ * @param schema - Schema configuration
+ * @param field - Form field name
+ * @param defaultError - Default error message
+ * @returns The merged field configuration
+ * @internal
+ */
+const mergeFieldConfig = (
+ schema: InvitationCreateSchemas | undefined,
+ field: 'email',
+ defaultError: string,
+) => {
+ const fieldConfig = schema?.[field];
+ return fieldConfig
+ ? {
+ ...fieldConfig,
+ errorMessage: fieldConfig.errorMessage || defaultError,
+ }
+ : {
+ errorMessage: defaultError,
+ };
+};
+
+/**
+ * Creates a schema for invitation create form validation.
+ * @param options - Schema configuration options (consumers can override)
+ * @param defaultEmailError - Default error message for invalid email
+ * @returns Object containing Zod email schema, email regex, and maxEmails
+ */
+export const createInvitationCreateSchema = (
+ options: InvitationCreateSchemas = {},
+ defaultEmailError = 'Please enter a valid email address',
+) => {
+ const emailConfig = mergeFieldConfig(options, 'email', defaultEmailError);
+
+ const emailRegex = emailConfig.regex ?? DEFAULT_EMAIL_REGEX;
+ const emailErrorMessage = emailConfig.errorMessage ?? defaultEmailError;
+ const maxEmails = options.maxEmails ?? DEFAULT_MAX_EMAILS;
+
+ const emailSchema = z.string().min(1, emailErrorMessage).regex(emailRegex, emailErrorMessage);
+
+ return {
+ emailSchema,
+ emailRegex,
+ emailErrorMessage,
+ maxEmails,
+ };
+};
+
+/**
+ * Default invitation create schema configuration.
+ */
+export const invitationCreateSchemaDefaults = createInvitationCreateSchema();
+
+/**
+ * Type for a validated email value.
+ */
+export type InternalInvitationEmailValue = z.infer<
+ typeof invitationCreateSchemaDefaults.emailSchema
+>;
diff --git a/packages/core/src/schemas/my-organization/member-management/invitations/invitation-schema.ts b/packages/core/src/schemas/my-organization/member-management/invitations/invitation-schema.ts
new file mode 100644
index 000000000..c5143b8c3
--- /dev/null
+++ b/packages/core/src/schemas/my-organization/member-management/invitations/invitation-schema.ts
@@ -0,0 +1,61 @@
+/**
+ * Invitation validation schemas.
+ * @module invitation-schema
+ * @internal
+ */
+
+import { z } from 'zod';
+
+/**
+ * Schema for organization invitation data.
+ * @internal
+ */
+export const invitationSchema = z.object({
+ id: z.string(),
+ invitee: z.object({
+ email: z.string().email(),
+ }),
+ inviter: z.object({
+ name: z.string().optional(),
+ }),
+ roles: z.array(z.string()).optional(),
+ created_at: z.string().optional(),
+ expires_at: z.string().optional(),
+});
+
+/**
+ * Schema for invitation list response.
+ * @internal
+ */
+export const invitationListResponseSchema = z.object({
+ invitations: z.array(invitationSchema),
+ total: z.number().optional(),
+ start: z.number().optional(),
+ limit: z.number().optional(),
+});
+
+/**
+ * Schema for creating invitation(s)
+ * @internal
+ */
+export const createInvitationSchema = z.object({
+ invitees: z.array(
+ z.object({
+ email: z.string().email(),
+ roles: z.array(z.string()).optional(),
+ }),
+ ),
+});
+
+/**
+ * Schema for revoking an invitation.
+ * @internal
+ */
+export const revokeInvitationSchema = z.object({
+ invitation_id: z.string(),
+});
+
+export type Invitation = z.infer;
+export type InvitationListResponse = z.infer;
+export type CreateInvitationInput = z.infer;
+export type RevokeInvitationInput = z.infer;
diff --git a/packages/react/src/hooks/my-organization/use-config.ts b/packages/react/src/hooks/my-organization/use-config.ts
index f3db4455f..01c57aa0e 100644
--- a/packages/react/src/hooks/my-organization/use-config.ts
+++ b/packages/react/src/hooks/my-organization/use-config.ts
@@ -58,5 +58,6 @@ export function useConfig(): UseConfigResult {
filteredStrategies,
shouldAllowDeletion,
isConfigValid,
+ allowedRoles: [],
};
}
diff --git a/packages/react/src/hooks/shared/__tests__/use-checkpoint-pagination.test.ts b/packages/react/src/hooks/shared/__tests__/use-checkpoint-pagination.test.ts
new file mode 100644
index 000000000..e38d795e3
--- /dev/null
+++ b/packages/react/src/hooks/shared/__tests__/use-checkpoint-pagination.test.ts
@@ -0,0 +1,235 @@
+import { renderHook, act } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+
+import { useCheckpointPagination } from '@/hooks/shared/use-checkpoint-pagination';
+import type { CheckpointPaginationOptions } from '@/hooks/shared/use-checkpoint-pagination';
+
+interface TestFilter {
+ roleId?: string;
+ searchQuery?: string;
+}
+
+const renderPagination = (options?: CheckpointPaginationOptions) =>
+ renderHook(() => useCheckpointPagination(options));
+
+describe('useCheckpointPagination', () => {
+ describe('Initial State', () => {
+ it('should initialize with default values', () => {
+ const { result } = renderPagination();
+
+ expect(result.current.pageSize).toBe(10);
+ expect(result.current.currentPage).toBe(1);
+ expect(result.current.fromToken).toBeUndefined();
+ expect(result.current.hasPreviousPage).toBe(false);
+ expect(result.current.sortConfig).toEqual({ key: null, direction: 'asc' });
+ expect(result.current.filters).toEqual({});
+ });
+
+ it('should initialize with custom defaults', () => {
+ const { result } = renderPagination({
+ defaultPageSize: 25,
+ defaultSortConfig: { key: 'created_at', direction: 'desc' },
+ defaultFilters: { roleId: 'role_admin' },
+ });
+
+ expect(result.current.pageSize).toBe(25);
+ expect(result.current.sortConfig).toEqual({ key: 'created_at', direction: 'desc' });
+ expect(result.current.filters).toEqual({ roleId: 'role_admin' });
+ });
+ });
+
+ describe('goToNextPage', () => {
+ it('should advance to the next page with a token', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ });
+
+ expect(result.current.currentPage).toBe(2);
+ expect(result.current.fromToken).toBe('token_page2');
+ expect(result.current.hasPreviousPage).toBe(true);
+ });
+
+ it('should track multiple page navigations', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ });
+ act(() => {
+ result.current.goToNextPage('token_page3');
+ });
+
+ expect(result.current.currentPage).toBe(3);
+ expect(result.current.fromToken).toBe('token_page3');
+ expect(result.current.hasPreviousPage).toBe(true);
+ });
+ });
+
+ describe('goToPreviousPage', () => {
+ it('should go back to the previous page', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ });
+ act(() => {
+ result.current.goToPreviousPage();
+ });
+
+ expect(result.current.currentPage).toBe(1);
+ expect(result.current.fromToken).toBeUndefined();
+ expect(result.current.hasPreviousPage).toBe(false);
+ });
+
+ it('should navigate back through multiple pages correctly', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ });
+ act(() => {
+ result.current.goToNextPage('token_page3');
+ });
+ act(() => {
+ result.current.goToPreviousPage();
+ });
+
+ expect(result.current.currentPage).toBe(2);
+ expect(result.current.fromToken).toBe('token_page2');
+ expect(result.current.hasPreviousPage).toBe(true);
+ });
+
+ it('should not go below page 1', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToPreviousPage();
+ });
+
+ expect(result.current.currentPage).toBe(1);
+ });
+ });
+
+ describe('changePageSize', () => {
+ it('should update page size and reset pagination', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ });
+ act(() => {
+ result.current.changePageSize(25);
+ });
+
+ expect(result.current.pageSize).toBe(25);
+ expect(result.current.currentPage).toBe(1);
+ expect(result.current.fromToken).toBeUndefined();
+ expect(result.current.hasPreviousPage).toBe(false);
+ });
+ });
+
+ describe('changeSortConfig', () => {
+ it('should update sort config and reset pagination', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ });
+ act(() => {
+ result.current.changeSortConfig({ key: 'created_at', direction: 'desc' });
+ });
+
+ expect(result.current.sortConfig).toEqual({ key: 'created_at', direction: 'desc' });
+ expect(result.current.currentPage).toBe(1);
+ expect(result.current.fromToken).toBeUndefined();
+ expect(result.current.hasPreviousPage).toBe(false);
+ });
+ });
+
+ describe('changeFilters', () => {
+ it('should update filters with a direct value and reset pagination', () => {
+ const { result } = renderPagination();
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ });
+ act(() => {
+ result.current.changeFilters({ roleId: 'role_admin' });
+ });
+
+ expect(result.current.filters).toEqual({ roleId: 'role_admin' });
+ expect(result.current.currentPage).toBe(1);
+ expect(result.current.fromToken).toBeUndefined();
+ });
+
+ it('should update filters with an updater function', () => {
+ const { result } = renderPagination({
+ defaultFilters: { searchQuery: 'test' },
+ });
+
+ act(() => {
+ result.current.changeFilters((prev) => ({ ...prev, roleId: 'role_member' }));
+ });
+
+ expect(result.current.filters).toEqual({ searchQuery: 'test', roleId: 'role_member' });
+ });
+ });
+
+ describe('reset', () => {
+ it('should reset all state to defaults', () => {
+ const { result } = renderPagination({
+ defaultPageSize: 10,
+ defaultSortConfig: { key: null, direction: 'asc' },
+ defaultFilters: {},
+ });
+
+ act(() => {
+ result.current.goToNextPage('token_page2');
+ result.current.changePageSize(50);
+ });
+ act(() => {
+ result.current.changeSortConfig({ key: 'created_at', direction: 'desc' });
+ });
+ act(() => {
+ result.current.changeFilters({ roleId: 'role_admin' });
+ });
+ act(() => {
+ result.current.reset();
+ });
+
+ expect(result.current.pageSize).toBe(10);
+ expect(result.current.currentPage).toBe(1);
+ expect(result.current.fromToken).toBeUndefined();
+ expect(result.current.hasPreviousPage).toBe(false);
+ expect(result.current.sortConfig).toEqual({ key: null, direction: 'asc' });
+ expect(result.current.filters).toEqual({});
+ });
+
+ it('should reset to custom defaults when provided', () => {
+ const { result } = renderPagination({
+ defaultPageSize: 25,
+ defaultSortConfig: { key: 'created_at', direction: 'desc' },
+ defaultFilters: { roleId: 'role_admin' },
+ });
+
+ act(() => {
+ result.current.changePageSize(50);
+ });
+ act(() => {
+ result.current.changeSortConfig({ key: null, direction: 'asc' });
+ });
+ act(() => {
+ result.current.changeFilters({});
+ });
+ act(() => {
+ result.current.reset();
+ });
+
+ expect(result.current.pageSize).toBe(25);
+ expect(result.current.sortConfig).toEqual({ key: 'created_at', direction: 'desc' });
+ expect(result.current.filters).toEqual({ roleId: 'role_admin' });
+ });
+ });
+});
diff --git a/packages/react/src/hooks/shared/use-checkpoint-pagination.ts b/packages/react/src/hooks/shared/use-checkpoint-pagination.ts
new file mode 100644
index 000000000..1d504d4e4
--- /dev/null
+++ b/packages/react/src/hooks/shared/use-checkpoint-pagination.ts
@@ -0,0 +1,131 @@
+import * as React from 'react';
+
+interface SortConfig {
+ key: string | null;
+ direction: 'asc' | 'desc';
+}
+
+export interface CheckpointPaginationOptions {
+ defaultPageSize?: number;
+ defaultSortConfig?: SortConfig;
+ defaultFilters?: TFilter;
+}
+
+export interface UseCheckpointPaginationResult {
+ pageSize: number;
+ fromToken: string | undefined;
+ sortConfig: SortConfig;
+ filters: TFilter;
+ currentPage: number;
+ hasPreviousPage: boolean;
+ goToNextPage: (nextToken: string) => void;
+ goToPreviousPage: () => void;
+ changePageSize: (pageSize: number) => void;
+ changeSortConfig: (sortConfig: SortConfig) => void;
+ changeFilters: (updater: TFilter | ((prev: TFilter) => TFilter)) => void;
+ reset: () => void;
+}
+
+const DEFAULT_PAGE_SIZE = 10;
+const DEFAULT_SORT_CONFIG: SortConfig = { key: null, direction: 'asc' };
+
+/**
+ * Hook for checkpoint-based pagination with sort, filter, and auto-reset.
+ * @param options - Pagination configuration options.
+ * @returns Pagination state and control functions.
+ */
+export function useCheckpointPagination>(
+ options: CheckpointPaginationOptions = {},
+): UseCheckpointPaginationResult {
+ const {
+ defaultPageSize = DEFAULT_PAGE_SIZE,
+ defaultSortConfig = DEFAULT_SORT_CONFIG,
+ defaultFilters = {} as TFilter,
+ } = options;
+
+ const [pageSize, setPageSize] = React.useState(defaultPageSize);
+ const [cursor, setCursor] = React.useState<{
+ fromToken: string | undefined;
+ previousTokens: (string | undefined)[];
+ currentPage: number;
+ }>({
+ fromToken: undefined,
+ previousTokens: [],
+ currentPage: 1,
+ });
+ const [sortConfig, setSortConfig] = React.useState(defaultSortConfig);
+ const [filters, setFilters] = React.useState(defaultFilters);
+
+ const resetCursor = React.useCallback(() => {
+ setCursor({ fromToken: undefined, previousTokens: [], currentPage: 1 });
+ }, []);
+
+ const goToNextPage = React.useCallback((nextToken: string) => {
+ setCursor((prev) => ({
+ previousTokens: [...prev.previousTokens, prev.fromToken],
+ fromToken: nextToken,
+ currentPage: prev.currentPage + 1,
+ }));
+ }, []);
+
+ const goToPreviousPage = React.useCallback(() => {
+ setCursor((prev) => {
+ if (prev.previousTokens.length === 0) return prev;
+
+ const newStack = [...prev.previousTokens];
+ const prevToken = newStack.pop();
+
+ return {
+ previousTokens: newStack,
+ fromToken: prevToken,
+ currentPage: Math.max(1, prev.currentPage - 1),
+ };
+ });
+ }, []);
+
+ const changePageSize = React.useCallback(
+ (newPageSize: number) => {
+ setPageSize(newPageSize);
+ resetCursor();
+ },
+ [resetCursor],
+ );
+
+ const changeSortConfig = React.useCallback(
+ (newSortConfig: SortConfig) => {
+ setSortConfig(newSortConfig);
+ resetCursor();
+ },
+ [resetCursor],
+ );
+
+ const changeFilters = React.useCallback(
+ (updater: TFilter | ((prev: TFilter) => TFilter)) => {
+ setFilters(updater);
+ resetCursor();
+ },
+ [resetCursor],
+ );
+
+ const reset = React.useCallback(() => {
+ setPageSize(defaultPageSize);
+ setSortConfig(defaultSortConfig);
+ setFilters(defaultFilters);
+ resetCursor();
+ }, [defaultPageSize, defaultSortConfig, defaultFilters, resetCursor]);
+
+ return {
+ pageSize,
+ currentPage: cursor.currentPage,
+ fromToken: cursor.fromToken,
+ hasPreviousPage: cursor.previousTokens.length > 0,
+ sortConfig,
+ filters,
+ goToNextPage,
+ goToPreviousPage,
+ changePageSize,
+ changeSortConfig,
+ changeFilters,
+ reset,
+ };
+}
diff --git a/packages/react/src/internals/__mocks__/my-organization/config/config.mocks.ts b/packages/react/src/internals/__mocks__/my-organization/config/config.mocks.ts
index dd682ce97..2b0f13b39 100644
--- a/packages/react/src/internals/__mocks__/my-organization/config/config.mocks.ts
+++ b/packages/react/src/internals/__mocks__/my-organization/config/config.mocks.ts
@@ -20,5 +20,6 @@ export const createMockUseConfig = (overrides?: Partial): MockUse
},
fetchConfig: vi.fn(async () => undefined),
filteredStrategies: [],
+ allowedRoles: [],
...overrides,
});
diff --git a/packages/react/src/tests/utils/__mocks__/my-organization/config/config.mocks.ts b/packages/react/src/tests/utils/__mocks__/my-organization/config/config.mocks.ts
index 102991ff4..9a78bfa91 100644
--- a/packages/react/src/tests/utils/__mocks__/my-organization/config/config.mocks.ts
+++ b/packages/react/src/tests/utils/__mocks__/my-organization/config/config.mocks.ts
@@ -14,5 +14,6 @@ export const createMockUseConfig = (overrides?: Partial): MockUse
},
fetchConfig: vi.fn(async () => undefined),
filteredStrategies: [],
+ allowedRoles: [],
...overrides,
});
diff --git a/packages/react/src/types/my-organization/config/config-types.ts b/packages/react/src/types/my-organization/config/config-types.ts
index b305c8cd9..3b7a27cba 100644
--- a/packages/react/src/types/my-organization/config/config-types.ts
+++ b/packages/react/src/types/my-organization/config/config-types.ts
@@ -8,6 +8,13 @@ import type {
IdpStrategy,
} from '@auth0/universal-components-core';
+/** Role returned from organization configuration. */
+export interface ConfigRole {
+ id: string;
+ name: string;
+ description?: string;
+}
+
/** useConfig hook result. */
export interface UseConfigResult {
config: GetConfigurationResponseContent | null;
@@ -16,4 +23,5 @@ export interface UseConfigResult {
filteredStrategies: IdpStrategy[];
shouldAllowDeletion: boolean;
isConfigValid: boolean;
+ allowedRoles: ConfigRole[];
}