diff --git a/packages/react/src/blocks/index.ts b/packages/react/src/blocks/index.ts index 5fcdea1c..78c873e3 100644 --- a/packages/react/src/blocks/index.ts +++ b/packages/react/src/blocks/index.ts @@ -3,4 +3,4 @@ export { OrgDetailsEdit } from './my-org/org-management/org-details-edit'; export { SsoProviderTable } from './my-org/idp-management/sso-provider-table'; export { SsoProviderCreate } from './my-org/idp-management/sso-provider-create'; export { SsoProviderEdit } from './my-org/idp-management/sso-provider-edit'; -export { DomainTable } from './my-org/domain-management/domain-table'; +export { DomainTable, DomainTableUI } from './my-org/domain-management'; diff --git a/packages/react/src/blocks/my-org/domain-management/domain-table-ui.tsx b/packages/react/src/blocks/my-org/domain-management/domain-table-ui.tsx new file mode 100644 index 00000000..776443e0 --- /dev/null +++ b/packages/react/src/blocks/my-org/domain-management/domain-table-ui.tsx @@ -0,0 +1,210 @@ +import { getComponentStyles, type Domain } from '@auth0/universal-components-core'; +import { useTheme, useTranslator } from '@react/hooks'; +import type { DomainTableUIProps } from '@react/types'; +import { Plus } from 'lucide-react'; +import * as React from 'react'; + +import { DomainConfigureProvidersModal } from '../../../components/my-org/domain-management/domain-configure/domain-configure-providers-modal'; +import { DomainCreateModal } from '../../../components/my-org/domain-management/domain-create/domain-create-modal'; +import { DomainDeleteModal } from '../../../components/my-org/domain-management/domain-delete/domain-delete-modal'; +import { DomainTableActionsColumn } from '../../../components/my-org/domain-management/domain-table/domain-table-actions-column'; +import { DomainVerifyModal } from '../../../components/my-org/domain-management/domain-verify/domain-verify-modal'; +import { Badge } from '../../../components/ui/badge'; +import { DataTable, type Column } from '../../../components/ui/data-table'; +import { Header } from '../../../components/ui/header'; +import { getStatusBadgeVariant } from '../../../lib/my-org/domain-management'; + +/** + * Domain Table UI component. + * + * Renders domain table component but requires external state management via the `logic` prop. + * Use this when you need custom Domain Table or want to integrate with your own state. + * Alternatively, you can use hooks like `useDomainTableLogic` or `useDomainTableState` to build custom logic + * without starting from scratch. + * + * @example + * ```tsx + * const customLogic = { + * state: { showCreateModal: false, setShowCreateModal: (show: boolean) => void }, + * actions: { handleCreate: (type) => {...}, handleDelete: (id) => {...} }, + * domainTableActions: { createAction: async () => {...} } + * }; + * + * + * ``` + */ +export function DomainTableUI({ + customMessages = {}, + schema, + hideHeader = false, + readOnly = false, + createAction, + onOpenProvider, + onCreateProvider, + styling = { + variables: { common: {}, light: {}, dark: {} }, + classes: {}, + }, + logic, +}: DomainTableUIProps) { + const { isDarkMode } = useTheme(); + const { t } = useTranslator('domain_management', customMessages); + + const { state, actions, domainTableActions } = logic; + const { + // State variables and methods + showCreateModal, + showConfigureModal, + showVerifyModal, + showDeleteModal, + verifyError, + selectedDomain, + setShowCreateModal, + setShowConfigureModal, + setShowDeleteModal, + } = state; + + const { + // Handlers + handleCreate, + handleVerify, + handleDelete, + handleToggleSwitch, + handleCloseVerifyModal, + handleCreateClick, + handleConfigureClick, + handleVerifyClick, + handleDeleteClick, + } = actions; + + const { + domains, + providers, + isCreating, + isVerifying, + isFetching, + isLoadingProviders, + isDeleting, + } = domainTableActions; + + const currentStyles = React.useMemo( + () => getComponentStyles(styling, isDarkMode), + [styling, isDarkMode], + ); + + const columns: Column[] = React.useMemo( + () => [ + { + type: 'text', + accessorKey: 'domain', + title: t('domain_table.table.columns.domain'), + width: '35%', + render: (domain) =>
{domain.domain}
, + }, + { + type: 'text', + accessorKey: 'status', + title: t('domain_table.table.columns.status'), + width: '25%', + render: (domain) => ( + + {t(`shared.domain_statuses.${domain.status}`)} + + ), + }, + { + type: 'actions', + title: '', + width: '20%', + render: (domain) => ( + + ), + }, + ], + [t, readOnly, customMessages, handleConfigureClick, handleVerifyClick, handleDeleteClick], + ); + + return ( +
+ {!hideHeader && ( +
+
handleCreateClick(), + icon: Plus, + disabled: createAction?.disabled || readOnly || isFetching, + }, + ]} + /> +
+ )} + + + + setShowCreateModal(false)} + onCreate={handleCreate} + customMessages={customMessages.create} + /> + + setShowConfigureModal(false)} + onToggleSwitch={handleToggleSwitch} + onOpenProvider={onOpenProvider} + onCreateProvider={onCreateProvider} + customMessages={customMessages.configure} + /> + + + + setShowDeleteModal(false)} + onDelete={handleDelete} + customMessages={customMessages.delete} + /> +
+ ); +} diff --git a/packages/react/src/blocks/my-org/domain-management/domain-table.tsx b/packages/react/src/blocks/my-org/domain-management/domain-table.tsx index cf23a70d..490ee718 100644 --- a/packages/react/src/blocks/my-org/domain-management/domain-table.tsx +++ b/packages/react/src/blocks/my-org/domain-management/domain-table.tsx @@ -1,224 +1,28 @@ -import { - type Domain, - getComponentStyles, - MY_ORG_DOMAIN_SCOPES, -} from '@auth0/universal-components-core'; -import { Plus } from 'lucide-react'; -import * as React from 'react'; +import { MY_ORG_DOMAIN_SCOPES } from '@auth0/universal-components-core'; +import type { DomainTableProps } from '@react/types'; +import type * as React from 'react'; -import { DomainConfigureProvidersModal } from '../../../components/my-org/domain-management/domain-configure/domain-configure-providers-modal'; -import { DomainCreateModal } from '../../../components/my-org/domain-management/domain-create/domain-create-modal'; -import { DomainDeleteModal } from '../../../components/my-org/domain-management/domain-delete/domain-delete-modal'; -import { DomainTableActionsColumn } from '../../../components/my-org/domain-management/domain-table/domain-table-actions-column'; -import { DomainVerifyModal } from '../../../components/my-org/domain-management/domain-verify/domain-verify-modal'; -import { Badge } from '../../../components/ui/badge'; -import { DataTable, type Column } from '../../../components/ui/data-table'; -import { Header } from '../../../components/ui/header'; +import { withDomainTableLogic } from '../../../hoc/with-domain-table'; import { withMyOrgService } from '../../../hoc/with-services'; -import { useDomainTable } from '../../../hooks/my-org/domain-management/use-domain-table'; -import { useDomainTableLogic } from '../../../hooks/my-org/domain-management/use-domain-table-logic'; -import { useTheme } from '../../../hooks/use-theme'; -import { useTranslator } from '../../../hooks/use-translator'; -import { getStatusBadgeVariant } from '../../../lib/my-org/domain-management'; -import type { DomainTableProps } from '../../../types/my-org/domain-management/domain-table-types'; - -/** - * DomainTable Component - */ -function DomainTableComponent({ - customMessages = {}, - schema, - styling = { - variables: { common: {}, light: {}, dark: {} }, - classes: {}, - }, - hideHeader = false, - readOnly = false, - createAction, - verifyAction, - deleteAction, - associateToProviderAction, - deleteFromProviderAction, - onOpenProvider, - onCreateProvider, -}: DomainTableProps) { - const { isDarkMode } = useTheme(); - const { t } = useTranslator('domain_management', customMessages); - - const { - domains, - providers, - isFetching, - isCreating, - isVerifying, - isDeleting, - isLoadingProviders, - fetchProviders, - fetchDomains, - onCreateDomain, - onVerifyDomain, - onDeleteDomain, - onAssociateToProvider, - onDeleteFromProvider, - } = useDomainTable({ - createAction, - verifyAction, - deleteAction, - associateToProviderAction, - deleteFromProviderAction, - customMessages, - }); - - const { - showCreateModal, - showConfigureModal, - showVerifyModal, - showDeleteModal, - verifyError, - selectedDomain, - setShowCreateModal, - setShowConfigureModal, - setShowDeleteModal, - handleCreate, - handleVerify, - handleDelete, - handleToggleSwitch, - handleCloseVerifyModal, - handleCreateClick, - handleConfigureClick, - handleVerifyClick, - handleDeleteClick, - } = useDomainTableLogic({ - t, - onCreateDomain, - onVerifyDomain, - onDeleteDomain, - onAssociateToProvider, - onDeleteFromProvider, - fetchProviders, - fetchDomains, - }); - - const currentStyles = React.useMemo( - () => getComponentStyles(styling, isDarkMode), - [styling, isDarkMode], - ); - const columns: Column[] = React.useMemo( - () => [ - { - type: 'text', - accessorKey: 'domain', - title: t('domain_table.table.columns.domain'), - width: '35%', - render: (domain) =>
{domain.domain}
, - }, - { - type: 'text', - accessorKey: 'status', - title: t('domain_table.table.columns.status'), - width: '25%', - render: (domain) => ( - - {t(`shared.domain_statuses.${domain.status}`)} - - ), - }, - { - type: 'actions', - title: '', - width: '20%', - render: (domain) => ( - - ), - }, - ], - [t, readOnly, customMessages, handleConfigureClick, handleVerifyClick, handleDeleteClick], - ); +import { DomainTableUI } from './domain-table-ui'; - return ( -
- {!hideHeader && ( -
-
handleCreateClick(), - icon: Plus, - disabled: createAction?.disabled || readOnly || isFetching, - }, - ]} - /> -
- )} - - - - setShowCreateModal(false)} - onCreate={handleCreate} - customMessages={customMessages.create} - /> - - setShowConfigureModal(false)} - onToggleSwitch={handleToggleSwitch} - onOpenProvider={onOpenProvider} - onCreateProvider={onCreateProvider} - customMessages={customMessages.configure} - /> - - +// Apply HOCs: My Org Services (WrappedComponent, scopes) + Domain Table logic (state management, actions etc) +const DomainTableWithHOCs = withMyOrgService( + withDomainTableLogic(DomainTableUI), + MY_ORG_DOMAIN_SCOPES, +); - setShowDeleteModal(false)} - onDelete={handleDelete} - customMessages={customMessages.delete} - /> -
- ); -} +/** + * Complete Domain Table component. + * + * Handles all Domain Table logic internally - displays createm verify delete modals, select domains etc + * Use this for plug-and-play Domain Table with no external state needed. + * + * @example + * ```tsx + * + * ``` + */ -export const DomainTable = withMyOrgService(DomainTableComponent, MY_ORG_DOMAIN_SCOPES); +export const DomainTable = DomainTableWithHOCs as React.ComponentType; diff --git a/packages/react/src/blocks/my-org/domain-management/index.ts b/packages/react/src/blocks/my-org/domain-management/index.ts new file mode 100644 index 00000000..057a4738 --- /dev/null +++ b/packages/react/src/blocks/my-org/domain-management/index.ts @@ -0,0 +1,8 @@ +// Main components +export { DomainTable } from './domain-table'; +export { DomainTableUI } from './domain-table-ui'; + +// Hooks for custom implementations +export { useDomainTableLogic } from '@react/hooks/my-org/domain-management/use-domain-table-logic'; +export { useDomainTableState } from '@react/hooks/my-org/domain-management/use-domain-table-state'; +export * from '@react/hooks/my-org/domain-management/use-domain-table'; diff --git a/packages/react/src/hoc/index.ts b/packages/react/src/hoc/index.ts new file mode 100644 index 00000000..a6e03ade --- /dev/null +++ b/packages/react/src/hoc/index.ts @@ -0,0 +1,2 @@ +export { withCoreClient } from './with-core-client'; +export { withDomainTableLogic } from './with-domain-table'; diff --git a/packages/react/src/hoc/with-core-client.tsx b/packages/react/src/hoc/with-core-client.tsx new file mode 100644 index 00000000..3183e7a2 --- /dev/null +++ b/packages/react/src/hoc/with-core-client.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; + +import { Spinner } from '../components/ui/spinner'; +import { useComponentConfig, useCoreClient } from '../hooks'; + +/** + * Higher-Order Component that ensures the core client is initialized before rendering the wrapped component. + * + * This HOC is useful for components that depend on the core client (like MFA operations) + * and need to wait for it to be properly initialized before they can function. + * + * @param WrappedComponent - The component that requires core client to be initialized + * @returns A new component that handles core client initialization and loading states + */ +export function withCoreClient

( + WrappedComponent: React.ComponentType

, +): React.ComponentType

{ + const WithCoreClientComponent = (props: P) => { + const { + config: { loader }, + } = useComponentConfig(); + const { coreClient } = useCoreClient(); + + if (!coreClient) { + return <>{loader || }; + } + + return ; + }; + + // Set display name for better debugging + WithCoreClientComponent.displayName = WrappedComponent.displayName || WrappedComponent.name; + + return WithCoreClientComponent; +} diff --git a/packages/react/src/hoc/with-domain-table.tsx b/packages/react/src/hoc/with-domain-table.tsx new file mode 100644 index 00000000..42c9a958 --- /dev/null +++ b/packages/react/src/hoc/with-domain-table.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + +import { useDomainTable } from '../hooks/my-org/domain-management/use-domain-table'; +import type { + DomainTableProps, + UseDomainTableResult, +} from '../types/my-org/domain-management/domain-table-types'; + +/** + * Higher-order component that provides Domain Table logic to any component + * This abstracts all the state management and business logic + */ +export function withDomainTableLogic

( + Component: React.ComponentType

, +) { + return function WithDomainTableLogicComponent(props: P) { + const domainTableLogic = useDomainTable({ + customMessages: props.customMessages, + createAction: props.createAction, + verifyAction: props.verifyAction, + deleteAction: props.deleteAction, + associateToProviderAction: props.associateToProviderAction, + deleteFromProviderAction: props.deleteFromProviderAction, + }); + + return ; + }; +} diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index e0cae626..47da0ddd 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -1,3 +1,4 @@ +export { useComponentConfig, Auth0ComponentConfigContext } from './use-config'; export { useCoreClient, CoreClientContext } from './use-core-client'; export { useTranslator } from './use-translator'; export { useTheme } from './use-theme'; diff --git a/packages/react/src/hooks/my-org/domain-management/use-domain-table-logic.tsx b/packages/react/src/hooks/my-org/domain-management/use-domain-table-logic.tsx index 114bf3c0..1bf21df0 100644 --- a/packages/react/src/hooks/my-org/domain-management/use-domain-table-logic.tsx +++ b/packages/react/src/hooks/my-org/domain-management/use-domain-table-logic.tsx @@ -2,10 +2,7 @@ import { type Domain, type IdentityProvider } from '@auth0/universal-components- import { useCallback, useEffect, useState } from 'react'; import { showToast } from '../../../components/ui/toast'; -import type { - UseDomainTableLogicOptions, - UseDomainTableLogicResult, -} from '../../../types/my-org/domain-management/domain-table-types'; +import type { UseDomainTableLogicOptions } from '../../../types/my-org/domain-management/domain-table-types'; import { useErrorHandler } from '../../use-error-handler'; export function useDomainTableLogic({ @@ -17,7 +14,7 @@ export function useDomainTableLogic({ onDeleteFromProvider, fetchProviders, fetchDomains, -}: UseDomainTableLogicOptions): UseDomainTableLogicResult { +}: UseDomainTableLogicOptions) { const { handleError } = useErrorHandler(); const [showCreateModal, setShowCreateModal] = useState(false); const [showConfigureModal, setShowConfigureModal] = useState(false); diff --git a/packages/react/src/hooks/my-org/domain-management/use-domain-table-state.tsx b/packages/react/src/hooks/my-org/domain-management/use-domain-table-state.tsx new file mode 100644 index 00000000..2571fd1b --- /dev/null +++ b/packages/react/src/hooks/my-org/domain-management/use-domain-table-state.tsx @@ -0,0 +1,30 @@ +import type { Domain, DomainTableState, DomainTableStateActions } from '@react/types'; +import { useState } from 'react'; + +/** + * Hook for managing Domain Table internal state + * Use this hook when you want to manage the state yourself + */ +export function useDomainTableState(): DomainTableState & DomainTableStateActions { + const [showCreateModal, setShowCreateModal] = useState(false); + const [showConfigureModal, setShowConfigureModal] = useState(false); + const [showVerifyModal, setShowVerifyModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [verifyError, setVerifyError] = useState(undefined); + const [selectedDomain, setSelectedDomain] = useState(null); + + return { + verifyError, + selectedDomain, + showCreateModal, + showConfigureModal, + showVerifyModal, + showDeleteModal, + setShowCreateModal, + setShowConfigureModal, + setShowVerifyModal, + setShowDeleteModal, + setVerifyError, + setSelectedDomain, + }; +} diff --git a/packages/react/src/hooks/use-config.ts b/packages/react/src/hooks/use-config.ts new file mode 100644 index 00000000..e7946ca2 --- /dev/null +++ b/packages/react/src/hooks/use-config.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; + +import type { Auth0ComponentConfig } from '../types/auth-types'; + +export const Auth0ComponentConfigContext = React.createContext<{ + config: Auth0ComponentConfig; +}>({ + config: { + themeSettings: { mode: 'light' }, + customOverrides: {}, + loader: undefined, + }, +}); + +/** + * Hook to access the Auth0 component configuration from context. + */ +export function useComponentConfig(): { config: Auth0ComponentConfig } { + const context = React.useContext(Auth0ComponentConfigContext); + + if (!context) { + throw new Error('useComponentConfig must be used within an Auth0ComponentProvider'); + } + + return context; +} diff --git a/packages/react/src/types/auth-types.ts b/packages/react/src/types/auth-types.ts index d696508d..98d67784 100644 --- a/packages/react/src/types/auth-types.ts +++ b/packages/react/src/types/auth-types.ts @@ -1,8 +1,13 @@ -import type { AuthDetails } from '@auth0/universal-components-core'; +import type { AuthDetails as AuthDetailsCore } from '@auth0/universal-components-core'; import type * as React from 'react'; import type { I18nOptions } from './i18n-types'; -import type { ThemeSettings } from './theme-types'; +import type { CustomOverrides, ThemeSettings } from './theme-types'; + +/** + * Auth0 authentication details with optional React-specific properties. + */ +export type AuthDetails = Omit; /** * Props for the Auth0ComponentProvider component. @@ -10,6 +15,20 @@ import type { ThemeSettings } from './theme-types'; export interface Auth0ComponentProviderProps { i18n?: I18nOptions; themeSettings?: ThemeSettings; - authDetails: AuthDetails; + authDetails?: AuthDetails; + customOverrides?: CustomOverrides; loader?: React.ReactNode; } + +/** + * Props for the InternalProvider component. + */ +export interface InternalProviderProps { + i18n?: I18nOptions; + authDetails?: AuthDetails; +} + +/** + * Configuration for Auth0ComponentProvider excluding authentication details. + */ +export type Auth0ComponentConfig = Omit; diff --git a/packages/react/src/types/index.ts b/packages/react/src/types/index.ts index a7a1588e..ba74bfae 100644 --- a/packages/react/src/types/index.ts +++ b/packages/react/src/types/index.ts @@ -1,2 +1,5 @@ export * from './my-account/mfa/mfa-types'; export * from './my-org'; +export * from './auth-types'; +export * from './i18n-types'; +export * from './theme-types'; diff --git a/packages/react/src/types/my-org/domain-management/domain-table-types.ts b/packages/react/src/types/my-org/domain-management/domain-table-types.ts index cf5fa4c0..92817baa 100644 --- a/packages/react/src/types/my-org/domain-management/domain-table-types.ts +++ b/packages/react/src/types/my-org/domain-management/domain-table-types.ts @@ -13,6 +13,7 @@ import type { EnhancedTranslationFunction, IdentityProviderAssociatedWithDomain, } from '@auth0/universal-components-core'; +import type { useDomainTable } from '@react/hooks/my-org/domain-management/use-domain-table'; export type { Domain }; @@ -105,21 +106,29 @@ export interface UseDomainTableLogicOptions { fetchDomains: UseDomainTableResult['fetchDomains']; } -export interface UseDomainTableLogicResult { - // Modal state +// State management for Domain Table +export interface DomainTableState { showCreateModal: boolean; showConfigureModal: boolean; showVerifyModal: boolean; showDeleteModal: boolean; verifyError: string | undefined; selectedDomain: Domain | null; +} +// State Actions for Domain Table +export interface DomainTableStateActions { // State setters setShowCreateModal: (show: boolean) => void; setShowConfigureModal: (show: boolean) => void; setShowVerifyModal: (show: boolean) => void; setShowDeleteModal: (show: boolean) => void; + setSelectedDomain: (domain: Domain | null) => void; + setVerifyError: (error: string | undefined) => void; +} +// Logic management for Domain Table +export interface DomainTableLogicActions { // Handlers handleCreate: (domainUrl: string) => Promise; handleVerify: (domain: Domain) => Promise; @@ -131,3 +140,20 @@ export interface UseDomainTableLogicResult { handleVerifyClick: (domain: Domain) => Promise; handleDeleteClick: (domain: Domain) => void; } + +export interface UseDomainTableLogicResult + extends DomainTableState, + DomainTableStateActions, + DomainTableLogicActions {} + +// State and logic props for Domain Table +export interface DomainTableLogicProps { + state: DomainTableState & DomainTableStateActions; + actions: DomainTableLogicActions; + domainTableActions: ReturnType; +} + +// Presentational Domain Table component props +export interface DomainTableUIProps extends DomainTableProps { + logic: DomainTableLogicProps; +} diff --git a/packages/react/src/types/theme-types.ts b/packages/react/src/types/theme-types.ts index c681f3b0..0036d662 100644 --- a/packages/react/src/types/theme-types.ts +++ b/packages/react/src/types/theme-types.ts @@ -12,6 +12,26 @@ export interface ThemeSettings { variables?: StylingVariables; } +/** + * BrandingTheme + * + * Controlled UL branding configuration. + */ +export type BrandingTheme = { + mode?: 'light' | 'dark' | 'system'; + primaryColor?: string; + borderRadius?: number; + fontFamily?: string; + [key: string]: unknown; +}; + +/** + * CustomerOverrides + * + * Custom CSS variable overrides (e.g. "--button-radius": "6px"). + */ +export type CustomOverrides = Record; + /** * ThemeInput *