From 035f0b3da54c09654338d7df5d5dc889beb20a85 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 2 Jul 2025 11:08:15 +0530 Subject: [PATCH 01/31] fix: correct header assignment in initializeEmbeddedSignInFlow function --- packages/javascript/src/api/initializeEmbeddedSignInFlow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts b/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts index d2300f290..cfe26aaa0 100644 --- a/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts +++ b/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts @@ -77,9 +77,9 @@ const initializeEmbeddedSignInFlow = async ({ ...requestConfig, method: requestConfig.method || 'POST', headers: { + ...requestConfig.headers, 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', - ...requestConfig.headers, }, body: searchParams.toString(), }); From 0a48ce3c6d7d0561f3d3ba430038e95968dd3eb4 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 2 Jul 2025 16:48:17 +0530 Subject: [PATCH 02/31] feat(javascript): add transformBrandingPreferenceToTheme utility and corresponding tests --- packages/javascript/src/index.ts | 1 + ...transformBrandingPreferenceToTheme.test.ts | 444 ++++++++++++++++++ .../transformBrandingPreferenceToTheme.ts | 147 ++++++ 3 files changed, 592 insertions(+) create mode 100644 packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts create mode 100644 packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 9228d8deb..a20b6f9c0 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -132,5 +132,6 @@ export {default as resolveFieldType} from './utils/resolveFieldType'; export {default as resolveFieldName} from './utils/resolveFieldName'; export {default as processOpenIDScopes} from './utils/processOpenIDScopes'; export {default as withVendorCSSClassPrefix} from './utils/withVendorCSSClassPrefix'; +export {default as transformBrandingPreferenceToTheme} from './utils/transformBrandingPreferenceToTheme'; export {default as StorageManager} from './StorageManager'; diff --git a/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts b/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts new file mode 100644 index 000000000..8508517a5 --- /dev/null +++ b/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts @@ -0,0 +1,444 @@ +/** + * 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. + */ + +import {describe, it, expect} from 'vitest'; +import {transformBrandingPreferenceToTheme} from '../transformBrandingPreferenceToTheme'; +import {BrandingPreference} from '../../models/branding-preference'; + +describe('transformBrandingPreferenceToTheme', () => { + const mockBrandingPreference: BrandingPreference = { + locale: 'en-US', + name: 'dxlab', + preference: { + configs: { + isBrandingEnabled: true, + removeDefaultBranding: false, + }, + layout: { + activeLayout: 'centered', + }, + organizationDetails: { + displayName: '', + supportEmail: '', + }, + theme: { + activeTheme: 'LIGHT', + LIGHT: { + buttons: { + externalConnection: { + base: { + background: { + backgroundColor: '#FFFFFF', + }, + border: { + borderRadius: '8px', + }, + font: { + color: '#000000de', + }, + }, + }, + primary: { + base: { + border: { + borderRadius: '8px', + }, + font: { + color: '#ffffffe6', + }, + }, + }, + secondary: { + base: { + border: { + borderRadius: '8px', + }, + font: { + color: '#000000de', + }, + }, + }, + }, + colors: { + alerts: { + error: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#ffd8d8', + }, + info: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#eff7fd', + }, + neutral: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#f8f8f9', + }, + warning: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#fff6e7', + }, + }, + background: { + body: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#fbfbfb', + }, + surface: { + contrastText: '', + dark: '#F6F4F2', + inverted: '#212A32', + light: '#f9fafb', + main: '#ffffff', + }, + }, + illustrations: { + accent1: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#3865B5', + }, + accent2: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#19BECE', + }, + accent3: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#FFFFFF', + }, + primary: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#FF7300', + }, + secondary: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#E0E1E2', + }, + }, + outlined: { + default: '#dadce0', + }, + primary: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#2563eb', + }, + secondary: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#E0E1E2', + }, + text: { + primary: '#000000de', + secondary: '#00000066', + }, + }, + footer: { + border: { + borderColor: '', + }, + font: { + color: '', + }, + }, + images: { + favicon: {}, + logo: { + imgURL: + 'https://cdn.statically.io/gh/brionmario/javascript/refs/heads/next/samples/teamspace-react/public/teamspace-logo.png', + }, + myAccountLogo: { + title: 'Account', + }, + }, + inputs: { + base: { + background: { + backgroundColor: '#FFFFFF', + }, + border: { + borderColor: '', + borderRadius: '8px', + }, + font: { + color: '', + }, + labels: { + font: { + color: '', + }, + }, + }, + }, + loginBox: { + background: { + backgroundColor: '', + }, + border: { + borderColor: '', + borderRadius: '12px', + borderWidth: '1px', + }, + font: { + color: '', + }, + }, + loginPage: { + background: { + backgroundColor: '', + }, + font: { + color: '', + }, + }, + typography: { + font: { + fontFamily: 'Gilmer', + importURL: '', + }, + heading: { + font: { + color: '', + }, + }, + }, + }, + DARK: { + buttons: { + externalConnection: { + base: { + background: { + backgroundColor: '#24292e', + }, + border: { + borderRadius: '22px', + }, + font: { + color: '#ffffff', + }, + }, + }, + primary: { + base: { + border: { + borderRadius: '22px', + }, + font: { + color: '#ffffff', + }, + }, + }, + secondary: { + base: { + border: { + borderRadius: '22px', + }, + font: { + color: '#000000', + }, + }, + }, + }, + colors: { + alerts: { + error: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#ff000054', + }, + info: { + contrastText: '', + dark: '#01579b', + inverted: '', + light: '', + main: '#0288d1', + }, + }, + background: { + body: { + contrastText: '', + dark: '', + inverted: '', + light: '', + main: '#121212', + }, + surface: { + contrastText: '', + dark: '#1e1e1e', + inverted: '#ffffff', + light: '#2c2c2c', + main: '#1a1a1a', + }, + }, + primary: { + contrastText: '#ffffff', + dark: '#1976d2', + inverted: '', + light: '#42a5f5', + main: '#2196f3', + }, + secondary: { + contrastText: '#ffffff', + dark: '#388e3c', + inverted: '', + light: '#66bb6a', + main: '#4caf50', + }, + text: { + primary: '#ffffff', + secondary: '#b3b3b3', + }, + }, + }, + }, + }, + }; + + it('should transform branding preference to theme using active theme', () => { + const theme = transformBrandingPreferenceToTheme(mockBrandingPreference); + + expect(theme).toBeDefined(); + expect(theme.colors).toBeDefined(); + expect(theme.colors.primary.main).toBe('#2563eb'); + expect(theme.colors.secondary.main).toBe('#E0E1E2'); + expect(theme.colors.background.surface).toBe('#ffffff'); + expect(theme.colors.background.body.main).toBe('#fbfbfb'); + expect(theme.colors.text.primary).toBe('#000000de'); + expect(theme.colors.text.secondary).toBe('#00000066'); + expect(theme.colors.border).toBe('#dadce0'); + expect(theme.borderRadius.small).toBe('8px'); + expect(theme.cssVariables).toBeDefined(); + }); + + it('should force light theme when specified', () => { + const theme = transformBrandingPreferenceToTheme(mockBrandingPreference, 'light'); + + expect(theme.colors.primary.main).toBe('#2563eb'); + expect(theme.colors.background.surface).toBe('#ffffff'); + expect(theme.colors.text.primary).toBe('#000000de'); + }); + + it('should force dark theme when specified', () => { + const theme = transformBrandingPreferenceToTheme(mockBrandingPreference, 'dark'); + + expect(theme.colors.primary.main).toBe('#2196f3'); + expect(theme.colors.primary.contrastText).toBe('#ffffff'); + expect(theme.colors.background.surface).toBe('#1a1a1a'); + expect(theme.colors.background.body.main).toBe('#121212'); + expect(theme.colors.text.primary).toBe('#ffffff'); + expect(theme.colors.text.secondary).toBe('#b3b3b3'); + }); + + it('should handle empty branding preference', () => { + const emptyBrandingPreference: BrandingPreference = {}; + const theme = transformBrandingPreferenceToTheme(emptyBrandingPreference); + + expect(theme).toBeDefined(); + expect(theme.colors).toBeDefined(); + expect(theme.cssVariables).toBeDefined(); + }); + + it('should handle branding preference without theme config', () => { + const brandingPreferenceWithoutTheme: BrandingPreference = { + preference: { + configs: { + isBrandingEnabled: true, + }, + }, + }; + const theme = transformBrandingPreferenceToTheme(brandingPreferenceWithoutTheme); + + expect(theme).toBeDefined(); + expect(theme.colors).toBeDefined(); + expect(theme.cssVariables).toBeDefined(); + }); + + it('should handle branding preference with missing theme variant', () => { + const brandingPreferenceWithMissingVariant: BrandingPreference = { + preference: { + theme: { + activeTheme: 'NONEXISTENT', + }, + }, + }; + const theme = transformBrandingPreferenceToTheme(brandingPreferenceWithMissingVariant); + + expect(theme).toBeDefined(); + expect(theme.colors).toBeDefined(); + expect(theme.cssVariables).toBeDefined(); + }); + + it('should use default values for missing color properties', () => { + const minimalBrandingPreference: BrandingPreference = { + preference: { + theme: { + activeTheme: 'LIGHT', + LIGHT: { + colors: { + primary: { + main: '#ff0000', + }, + }, + }, + }, + }, + }; + const theme = transformBrandingPreferenceToTheme(minimalBrandingPreference); + + expect(theme.colors.primary.main).toBe('#ff0000'); + expect(theme.colors.primary.contrastText).toBe('#ffffff'); // Default value + expect(theme.colors.secondary.main).toBe('#424242'); // Default value + expect(theme.colors.error.main).toBe('#d32f2f'); // Default value + expect(theme.colors.success.main).toBe('#4caf50'); // Default value + expect(theme.colors.warning.main).toBe('#ff9800'); // Default value + }); +}); diff --git a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts new file mode 100644 index 000000000..d875fc7bf --- /dev/null +++ b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts @@ -0,0 +1,147 @@ +/** + * 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. + */ + +import {BrandingPreference, ThemeVariant} from '../models/branding-preference'; +import {Theme, ThemeConfig} from '../theme/types'; +import createTheme from '../theme/createTheme'; + +/** + * Safely extracts a color value from the branding preference structure + */ +const extractColorValue = (colorVariant?: {main?: string; contrastText?: string}) => { + return colorVariant?.main; +}; + +/** + * Safely extracts contrast text color from the branding preference structure + */ +const extractContrastText = (colorVariant?: {main?: string; contrastText?: string}) => { + return colorVariant?.contrastText; +}; + +/** + * Transforms a ThemeVariant from branding preference to ThemeConfig + */ +const transformThemeVariant = (themeVariant: ThemeVariant): Partial => { + const colors = themeVariant.colors; + const buttons = themeVariant.buttons; + const inputs = themeVariant.inputs; + + return { + colors: { + primary: { + main: extractColorValue(colors?.primary), + contrastText: extractContrastText(colors?.primary), + }, + secondary: { + main: extractColorValue(colors?.secondary), + contrastText: extractContrastText(colors?.secondary), + }, + background: { + surface: extractColorValue(colors?.background?.surface), + disabled: extractColorValue(colors?.background?.surface), + body: { + main: extractColorValue(colors?.background?.body), + }, + }, + text: { + primary: colors?.text?.primary, + secondary: colors?.text?.secondary, + }, + border: colors?.outlined?.default, + error: { + main: extractColorValue(colors?.alerts?.error), + contrastText: extractContrastText(colors?.alerts?.error), + }, + success: { + main: extractColorValue(colors?.alerts?.info), + contrastText: extractContrastText(colors?.alerts?.info), + }, + warning: { + main: extractColorValue(colors?.alerts?.warning), + contrastText: extractContrastText(colors?.alerts?.warning), + }, + }, + // Extract border radius from buttons or inputs + borderRadius: { + small: buttons?.primary?.base?.border?.borderRadius || inputs?.base?.border?.borderRadius, + medium: buttons?.secondary?.base?.border?.borderRadius, + large: buttons?.externalConnection?.base?.border?.borderRadius, + }, + }; +}; + +/** + * Transforms branding preference response to Theme object + * + * @param brandingPreference - The branding preference response from getBrandingPreference + * @param forceTheme - Optional parameter to force a specific theme ('light' or 'dark'), + * if not provided, will use the activeTheme from branding preference + * @returns Theme object that can be used with the theme system + * + * @example + * ```typescript + * const brandingPreference = await getBrandingPreference({ baseUrl: "..." }); + * const theme = transformBrandingPreferenceToTheme(brandingPreference); + * + * // Force light theme regardless of branding preference activeTheme + * const lightTheme = transformBrandingPreferenceToTheme(brandingPreference, 'light'); + * ``` + */ +export const transformBrandingPreferenceToTheme = ( + brandingPreference: BrandingPreference, + forceTheme?: 'light' | 'dark', +): Theme => { + // Extract theme configuration + const themeConfig = brandingPreference?.preference?.theme; + + if (!themeConfig) { + // If no theme config is provided, return default light theme + return createTheme({}, false); + } + + // Determine which theme variant to use + let activeThemeKey: string; + if (forceTheme) { + activeThemeKey = forceTheme.toUpperCase(); + } else { + activeThemeKey = themeConfig.activeTheme || 'LIGHT'; + } + + // Get the theme variant (LIGHT or DARK) + const themeVariant = themeConfig[activeThemeKey as keyof typeof themeConfig] as ThemeVariant; + + if (!themeVariant) { + // If the specified theme variant doesn't exist, fallback to light theme + const fallbackVariant = themeConfig.LIGHT || themeConfig.DARK; + if (fallbackVariant) { + const transformedConfig = transformThemeVariant(fallbackVariant); + return createTheme(transformedConfig, activeThemeKey === 'DARK'); + } + // If no theme variants exist, return default theme + return createTheme({}, activeThemeKey === 'DARK'); + } + + // Transform the theme variant to ThemeConfig + const transformedConfig = transformThemeVariant(themeVariant); + + // Create the theme using the transformed config + return createTheme(transformedConfig, activeThemeKey === 'DARK'); +}; + +export default transformBrandingPreferenceToTheme; From 88144bcefbcaef45b59ee5f9b99bf5e4bf1ed004 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 2 Jul 2025 16:49:47 +0530 Subject: [PATCH 03/31] chore(nextjs): add scopes handling in decorateConfigWithNextEnv utility --- packages/nextjs/src/utils/decorateConfigWithNextEnv.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts b/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts index 2c25fa431..e164d08af 100644 --- a/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts +++ b/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts @@ -19,10 +19,11 @@ import {AsgardeoNextConfig} from '../models/config'; const decorateConfigWithNextEnv = (config: AsgardeoNextConfig): AsgardeoNextConfig => { - const {organizationHandle, applicationId, baseUrl, clientId, clientSecret, signInUrl, signUpUrl, afterSignInUrl, afterSignOutUrl, ...rest} = config; + const {organizationHandle, scopes, applicationId, baseUrl, clientId, clientSecret, signInUrl, signUpUrl, afterSignInUrl, afterSignOutUrl, ...rest} = config; return { ...rest, + scopes: scopes || (process.env['NEXT_PUBLIC_ASGARDEO_SCOPES'] as string), 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), From b387c1f9481d8fb11a1ddba24cc9842c365ce89b Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 2 Jul 2025 16:51:43 +0530 Subject: [PATCH 04/31] chore: integrate branding support in ThemeProvider and add useBranding hook --- .../javascript/src/api/getAllOrganizations.ts | 2 +- .../UserProfile/BaseUserProfile.tsx | 4 - .../contexts/Asgardeo/AsgardeoProvider.tsx | 2 +- .../react/src/contexts/Theme/ThemeContext.ts | 12 + .../src/contexts/Theme/ThemeProvider.tsx | 161 ++++++++++- packages/react/src/hooks/useBranding.ts | 259 ++++++++++++++++++ packages/react/src/index.ts | 3 + .../src/components/Header/UserDropdown.tsx | 17 +- samples/teamspace-react/src/main.tsx | 24 +- 9 files changed, 455 insertions(+), 29 deletions(-) create mode 100644 packages/react/src/hooks/useBranding.ts diff --git a/packages/javascript/src/api/getAllOrganizations.ts b/packages/javascript/src/api/getAllOrganizations.ts index 0d9e53149..a755efbb4 100644 --- a/packages/javascript/src/api/getAllOrganizations.ts +++ b/packages/javascript/src/api/getAllOrganizations.ts @@ -150,9 +150,9 @@ const getAllOrganizations = async ({ ...requestConfig, method: 'GET', headers: { + ...requestConfig.headers, 'Content-Type': 'application/json', Accept: 'application/json', - ...requestConfig.headers, }, }; diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx index 50e59dd6b..b56f9027d 100644 --- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx @@ -82,7 +82,6 @@ const fieldsToSkip: string[] = [ 'roles.default', 'active', 'groups', - 'profileUrl', 'accountLocked', 'accountDisabled', 'oneTimePassword', @@ -670,9 +669,6 @@ const BaseUserProfile: FC = ({
{schemas .filter(schema => { - // Filter out avatar-related fields and fields we don't want to show - if (!schema.name || schema.name === 'profileUrl') return false; - // Skip fields that are in the fieldsToSkip array if (fieldsToSkip.includes(schema.name)) return false; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 73995d1bf..e31da3099 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -281,7 +281,7 @@ const AsgardeoProvider: FC> = ({ }} > - + void; + /** + * Whether branding theme is currently loading + */ + isBrandingLoading?: boolean; + /** + * Error from branding theme fetch, if any + */ + brandingError?: Error | null; + /** + * Whether branding inheritance is enabled + */ + inheritFromBranding?: boolean; } const ThemeContext = createContext(null); diff --git a/packages/react/src/contexts/Theme/ThemeProvider.tsx b/packages/react/src/contexts/Theme/ThemeProvider.tsx index 7677ff45d..4176475ad 100644 --- a/packages/react/src/contexts/Theme/ThemeProvider.tsx +++ b/packages/react/src/contexts/Theme/ThemeProvider.tsx @@ -27,8 +27,10 @@ import { createClassObserver, createMediaQueryListener, BrowserThemeDetection, + ThemePreferences, } from '@asgardeo/browser'; import ThemeContext from './ThemeContext'; +import useBranding, {UseBrandingConfig} from '../../hooks/useBranding'; export interface ThemeProviderProps { theme?: RecursivePartial; @@ -38,12 +40,21 @@ export interface ThemeProviderProps { * - 'dark': Always use dark theme * - 'system': Use system preference (prefers-color-scheme media query) * - 'class': Detect theme based on CSS classes on HTML element + * - 'branding': Use active theme from branding preference (requires inheritFromBranding=true) */ - mode?: ThemeMode; + mode?: ThemeMode | 'branding'; /** * Configuration for theme detection when using 'class' or 'system' mode */ detection?: BrowserThemeDetection; + /** + * Configuration for branding integration + */ + inheritFromBranding?: ThemePreferences['inheritFromBranding']; + /** + * Configuration for branding API call + */ + brandingConfig?: UseBrandingConfig; } const applyThemeToDOM = (theme: Theme) => { @@ -52,21 +63,152 @@ const applyThemeToDOM = (theme: Theme) => { }); }; +/** + * ThemeProvider component that manages theme state and provides theme context to child components. + * + * This provider integrates with Asgardeo branding preferences to automatically apply + * organization-specific themes while allowing for custom theme overrides. + * + * Features: + * - Automatic theme mode detection (light/dark/system/class) + * - Integration with Asgardeo branding API through useBranding hook + * - Merging of branding themes with custom theme configurations + * - CSS variable injection for easy styling + * - Loading and error states for branding integration + * + * @example + * Basic usage with branding integration: + * ```tsx + * + * + * + * ``` + * + * @example + * With custom theme overrides: + * ```tsx + * + * + * + * ``` + * + * @example + * With branding-driven theme mode: + * ```tsx + * + * + * + * ``` + * + * @example + * With custom branding configuration: + * ```tsx + * + * + * + * ``` + */ const ThemeProvider: FC> = ({ children, theme: themeConfig, mode = 'system', detection = {}, + inheritFromBranding = true, + brandingConfig, }: PropsWithChildren): ReactElement => { const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(() => { // Initialize with detected theme mode or fallback to defaultMode if (mode === 'light' || mode === 'dark') { return mode; } + // For 'branding' mode, start with system preference and update when branding loads + if (mode === 'branding') { + return detectThemeMode('system', detection); + } return detectThemeMode(mode, detection); }); - const theme = useMemo(() => createTheme(themeConfig, colorScheme === 'dark'), [themeConfig, colorScheme]); + // Use branding theme if inheritFromBranding is enabled + const { + theme: brandingTheme, + activeTheme: brandingActiveTheme, + isLoading: isBrandingLoading, + error: brandingError + } = useBranding({ + autoFetch: inheritFromBranding, + // Don't pass forceTheme initially, let branding determine the active theme + ...brandingConfig, // Allow override of branding configuration + }); + + // Update color scheme based on branding active theme when available + useEffect(() => { + if (inheritFromBranding && brandingActiveTheme) { + // Update color scheme based on mode preference + if (mode === 'branding') { + // Always follow branding active theme + setColorScheme(brandingActiveTheme); + } else if (mode === 'system' && !isBrandingLoading) { + // For system mode, prefer branding but allow system override if no branding + setColorScheme(brandingActiveTheme); + } + } + }, [inheritFromBranding, brandingActiveTheme, mode, isBrandingLoading]); + + // Merge user-provided theme config with branding theme + const finalThemeConfig = useMemo(() => { + if (!inheritFromBranding || !brandingTheme) { + return themeConfig; + } + + // Convert branding theme to our theme config format + const brandingThemeConfig: RecursivePartial = { + colors: brandingTheme.colors, + borderRadius: brandingTheme.borderRadius, + shadows: brandingTheme.shadows, + spacing: brandingTheme.spacing, + }; + + // Merge branding theme with user-provided theme config + // User-provided config takes precedence over branding + return { + ...brandingThemeConfig, + ...themeConfig, + colors: { + ...brandingThemeConfig.colors, + ...themeConfig?.colors, + }, + borderRadius: { + ...brandingThemeConfig.borderRadius, + ...themeConfig?.borderRadius, + }, + shadows: { + ...brandingThemeConfig.shadows, + ...themeConfig?.shadows, + }, + spacing: { + ...brandingThemeConfig.spacing, + ...themeConfig?.spacing, + }, + }; + }, [inheritFromBranding, brandingTheme, themeConfig]); + + const theme = useMemo(() => createTheme(finalThemeConfig, colorScheme === 'dark'), [finalThemeConfig, colorScheme]); const handleThemeChange = useCallback((isDark: boolean) => { setColorScheme(isDark ? 'dark' : 'light'); @@ -80,13 +222,21 @@ const ThemeProvider: FC> = ({ let observer: MutationObserver | null = null; let mediaQuery: MediaQueryList | null = null; + // Don't set up automatic theme detection for branding mode + if (mode === 'branding') { + return; + } + if (mode === 'class') { const targetElement = detection.targetElement || document.documentElement; if (targetElement) { observer = createClassObserver(targetElement, handleThemeChange, detection); } } else if (mode === 'system') { - mediaQuery = createMediaQueryListener(handleThemeChange); + // Only set up system listener if not using branding or branding hasn't loaded yet + if (!inheritFromBranding || !brandingActiveTheme) { + mediaQuery = createMediaQueryListener(handleThemeChange); + } } return () => { @@ -103,7 +253,7 @@ const ThemeProvider: FC> = ({ } } }; - }, [mode, detection, handleThemeChange]); + }, [mode, detection, handleThemeChange, inheritFromBranding, brandingActiveTheme]); useEffect(() => { applyThemeToDOM(theme); @@ -113,6 +263,9 @@ const ThemeProvider: FC> = ({ theme, colorScheme, toggleTheme, + isBrandingLoading, + brandingError, + inheritFromBranding, }; return {children}; diff --git a/packages/react/src/hooks/useBranding.ts b/packages/react/src/hooks/useBranding.ts new file mode 100644 index 000000000..7cbe6ae67 --- /dev/null +++ b/packages/react/src/hooks/useBranding.ts @@ -0,0 +1,259 @@ +/** + * 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. + */ + +import {useCallback, useEffect, useState} from 'react'; +import { + getBrandingPreference, + GetBrandingPreferenceConfig, + BrandingPreference, + Theme, + transformBrandingPreferenceToTheme, +} from '@asgardeo/browser'; +import useAsgardeo from '../contexts/Asgardeo/useAsgardeo'; + +/** + * Configuration options for the useBranding hook + */ +export interface UseBrandingConfig { + /** + * Locale for the branding preference + */ + locale?: string; + /** + * Name of the branding preference + */ + name?: string; + /** + * Type of the branding preference + */ + type?: string; + /** + * Force a specific theme ('light' or 'dark') + * If not provided, will use the activeTheme from branding preference + */ + forceTheme?: 'light' | 'dark'; + /** + * Whether to automatically fetch branding preference on mount + * @default true + */ + autoFetch?: boolean; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Return type of the useBranding hook + */ +export interface UseBrandingReturn { + /** + * The raw branding preference data + */ + brandingPreference: BrandingPreference | null; + /** + * The transformed theme object + */ + theme: Theme | null; + /** + * The active theme mode from branding preference ('light' | 'dark') + */ + activeTheme: 'light' | 'dark' | null; + /** + * Loading state + */ + isLoading: boolean; + /** + * Error state + */ + error: Error | null; + /** + * Function to manually fetch branding preference + */ + fetchBranding: () => Promise; + /** + * Function to refetch branding preference + * This bypasses the single-call restriction and forces a new API call + */ + refetch: () => Promise; +} + +/** + * React hook for fetching and transforming branding preferences from Asgardeo. + * This hook automatically fetches branding preferences using the configured + * base URL from the Asgardeo context and transforms them into a theme object. + * + * The hook ensures the branding API is called only once during the component lifecycle + * unless explicitly refetched using the refetch function. + * + * @param config - Configuration options for the hook + * @returns Object containing branding preference data, theme, loading state, error, and refetch function + * + * @example + * Basic usage: + * ```tsx + * function MyComponent() { + * const { theme, activeTheme, isLoading, error } = useBranding(); + * + * if (isLoading) return
Loading branding...
; + * if (error) return
Error: {error.message}
; + * + * return ( + *
+ *

Active theme mode: {activeTheme}

+ *

Styled with Asgardeo branding

+ *
+ * ); + * } + * ``` + * + * @example + * With custom configuration: + * ```tsx + * function MyComponent() { + * const { theme, fetchBranding } = useBranding({ + * locale: 'en-US', + * name: 'my-branding', + * type: 'org', + * forceTheme: 'dark', + * autoFetch: false + * }); + * + * useEffect(() => { + * fetchBranding(); + * }, [fetchBranding]); + * + * return
Custom branding component
; + * } + * ``` + * + * @example + * With custom fetcher: + * ```tsx + * function MyComponent() { + * const { theme, isLoading, error } = useBranding({ + * fetcher: async (url, config) => { + * // Use your custom HTTP client + * const response = await myHttpClient.request({ + * url, + * method: config.method, + * headers: config.headers, + * ...config + * }); + * return response; + * } + * }); + * + * return
Component with custom fetcher
; + * } + * ``` + */ +export const useBranding = (config: UseBrandingConfig = {}): UseBrandingReturn => { + const {locale, name, type, forceTheme, autoFetch = true, fetcher} = config; + + const {baseUrl, isInitialized} = useAsgardeo(); + + const [brandingPreference, setBrandingPreference] = useState(null); + const [theme, setTheme] = useState(null); + const [activeTheme, setActiveTheme] = useState<'light' | 'dark' | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [hasFetched, setHasFetched] = useState(false); + + const fetchBranding = useCallback(async (): Promise => { + if (!baseUrl) { + setError(new Error('Base URL is not available. Make sure you are using this hook within an AsgardeoProvider.')); + return; + } + + // Prevent multiple calls if already fetching or already fetched (unless explicitly called) + if (isLoading) { + return; + } + + setIsLoading(true); + setError(null); + + try { + const getBrandingConfig: GetBrandingPreferenceConfig = { + baseUrl, + locale, + name, + type, + fetcher, + }; + + const brandingData = await getBrandingPreference(getBrandingConfig); + setBrandingPreference(brandingData); + + // Extract active theme from branding preference + const activeThemeFromBranding = brandingData?.preference?.theme?.activeTheme; + let extractedActiveTheme: 'light' | 'dark' | null = null; + + if (activeThemeFromBranding) { + // Convert to lowercase and map to our expected values + const themeMode = activeThemeFromBranding.toLowerCase(); + if (themeMode === 'light' || themeMode === 'dark') { + extractedActiveTheme = themeMode; + } + } + + setActiveTheme(extractedActiveTheme); + + // Transform branding preference to theme + const transformedTheme = transformBrandingPreferenceToTheme(brandingData, forceTheme); + setTheme(transformedTheme); + setHasFetched(true); + } catch (err) { + const errorMessage = err instanceof Error ? err : new Error('Failed to fetch branding preference'); + setError(errorMessage); + setBrandingPreference(null); + setTheme(null); + setActiveTheme(null); + setHasFetched(true); // Mark as fetched even on error to prevent retries + } finally { + setIsLoading(false); + } + }, [baseUrl, locale, name, type, forceTheme, fetcher, isLoading]); + + // Auto-fetch when dependencies change - but only once + useEffect(() => { + if (autoFetch && isInitialized && baseUrl && !hasFetched) { + fetchBranding(); + } + }, [autoFetch, isInitialized, baseUrl, hasFetched, fetchBranding]); + + // Manual refetch function that bypasses the hasFetched check + const refetch = useCallback(async (): Promise => { + setHasFetched(false); // Reset the flag to allow refetching + await fetchBranding(); + }, [fetchBranding]); + + return { + brandingPreference, + theme, + activeTheme, + isLoading, + error, + fetchBranding, + refetch, + }; +}; + +export default useBranding; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 0945cf56f..5b0d033c1 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -79,6 +79,9 @@ export * from './hooks/useTranslation'; export {default as useForm} from './hooks/useForm'; export * from './hooks/useForm'; +export {default as useBranding} from './hooks/useBranding'; +export * from './hooks/useBranding'; + export {default as BaseSignInButton} from './components/actions/SignInButton/BaseSignInButton'; export * from './components/actions/SignInButton/BaseSignInButton'; diff --git a/samples/teamspace-react/src/components/Header/UserDropdown.tsx b/samples/teamspace-react/src/components/Header/UserDropdown.tsx index c50f36501..100237b51 100644 --- a/samples/teamspace-react/src/components/Header/UserDropdown.tsx +++ b/samples/teamspace-react/src/components/Header/UserDropdown.tsx @@ -1,6 +1,6 @@ 'use client'; -import {ChevronDown, CogIcon, LogOut, Settings, UserIcon} from 'lucide-react'; +import {ChevronDown, CogIcon, LogOut, Settings, UserIcon, Workflow, LayoutDashboard} from 'lucide-react'; import {UserDropdown as _UserDropdown, SignOutButton, UserProfile} from '@asgardeo/react'; import {PoundSterling} from 'lucide-react'; import {useState, useRef} from 'react'; @@ -98,8 +98,8 @@ export default function UserDropdown({mode = 'default'}: UserDropdownProps) { { label: ( - - Billing + + Workflows ), onClick: () => null, @@ -107,7 +107,16 @@ export default function UserDropdown({mode = 'default'}: UserDropdownProps) { { label: ( - + + Dashboard + + ), + href: '/dashboard', + }, + { + label: ( + + Settings ), diff --git a/samples/teamspace-react/src/main.tsx b/samples/teamspace-react/src/main.tsx index 843bf58f1..826216fec 100644 --- a/samples/teamspace-react/src/main.tsx +++ b/samples/teamspace-react/src/main.tsx @@ -12,23 +12,17 @@ createRoot(document.getElementById('root')!).render( afterSignOutUrl={import.meta.env.VITE_ASGARDEO_AFTER_SIGN_OUT_URL} clientId={import.meta.env.VITE_ASGARDEO_CLIENT_ID} signInUrl={import.meta.env.VITE_ASGARDEO_SIGN_IN_URL} - scopes={[ - 'openid', - 'address', - 'email', - 'profile', - 'user:email', - 'read:user', - 'internal_organization_create', - 'internal_org_organization_create', - 'internal_organization_view', - 'internal_org_organization_view', - 'internal_organization_update', - 'internal_organization_delete', - 'internal_org_organization_delete', - ]} + scopes="openid address email profile user:email read:user internal_organization_create internal_organization_view internal_organization_update internal_organization_delete internal_org_organization_update internal_org_organization_create internal_org_organization_view internal_org_organization_delete" preferences={{ theme: { + overrides: { + colors: { + primary: { + main: '#1976d2', // Custom primary color + contrastText: 'white', + }, + }, + }, mode: 'light', // This will detect theme based on CSS classes // You can also use other modes: // mode: 'system', // Follows system preference (prefers-color-scheme) From 931f17aa5c6effeb917d49fb53d8739d575860ce Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 2 Jul 2025 18:05:39 +0530 Subject: [PATCH 05/31] feat(react): enhance organization translations and update rendering logic in OrganizationList component --- packages/javascript/src/i18n/en-US.ts | 12 +- packages/javascript/src/models/i18n.ts | 10 ++ .../OrganizationList/BaseOrganizationList.tsx | 162 +++++++++++++----- .../OrganizationList/OrganizationList.tsx | 71 +------- .../UserProfile/BaseUserProfile.tsx | 7 +- .../Organization/OrganizationProvider.tsx | 16 -- .../src/contexts/Theme/ThemeProvider.tsx | 10 +- 7 files changed, 147 insertions(+), 141 deletions(-) diff --git a/packages/javascript/src/i18n/en-US.ts b/packages/javascript/src/i18n/en-US.ts index c327ea5ca..31242ddab 100644 --- a/packages/javascript/src/i18n/en-US.ts +++ b/packages/javascript/src/i18n/en-US.ts @@ -88,8 +88,18 @@ const translations: I18nTranslations = { 'organization.switcher.members': 'members', 'organization.switcher.member': 'member', 'organization.switcher.create.organization': 'Create Organization', - 'organization.switcher.manage.organizations': 'Manage Organization', + 'organization.switcher.manage.organizations': 'Manage Organizations', 'organization.switcher.manage.button': 'Manage', + 'organization.switcher.organizations.title': 'Organizations', + 'organization.switcher.switch.button': 'Switch', + 'organization.switcher.no.access': 'No Access', + 'organization.switcher.status.label': 'Status:', + 'organization.switcher.showing.count': 'Showing {showing} of {total} organizations', + 'organization.switcher.refresh.button': 'Refresh', + 'organization.switcher.load.more': 'Load More Organizations', + 'organization.switcher.loading.more': 'Loading...', + 'organization.switcher.no.organizations': 'No organizations found', + 'organization.switcher.error.prefix': 'Error:', 'organization.profile.title': 'Organization Profile', 'organization.profile.loading': 'Loading organization...', 'organization.profile.error': 'Failed to load organization', diff --git a/packages/javascript/src/models/i18n.ts b/packages/javascript/src/models/i18n.ts index 1df787845..dbdf9d278 100644 --- a/packages/javascript/src/models/i18n.ts +++ b/packages/javascript/src/models/i18n.ts @@ -88,6 +88,16 @@ export interface I18nTranslations { 'organization.switcher.create.organization': string; 'organization.switcher.manage.organizations': string; 'organization.switcher.manage.button': string; + 'organization.switcher.organizations.title': string; + 'organization.switcher.switch.button': string; + 'organization.switcher.no.access': string; + 'organization.switcher.status.label': string; + 'organization.switcher.showing.count': string; + 'organization.switcher.refresh.button': string; + 'organization.switcher.load.more': string; + 'organization.switcher.loading.more': string; + 'organization.switcher.no.organizations': string; + 'organization.switcher.error.prefix': string; 'organization.profile.title': string; 'organization.profile.loading': string; 'organization.profile.error': string; diff --git a/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx b/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx index c555c2275..832da5e74 100644 --- a/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx +++ b/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx @@ -21,8 +21,12 @@ import clsx from 'clsx'; import {FC, ReactElement, ReactNode, useMemo, CSSProperties} from 'react'; import {OrganizationWithSwitchAccess} from '../../../contexts/Organization/OrganizationContext'; import useTheme from '../../../contexts/Theme/useTheme'; +import useTranslation from '../../../hooks/useTranslation'; import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; -import {Avatar} from '../../primitives/Avatar/Avatar'; +import Avatar from '../../primitives/Avatar/Avatar'; +import Button from '../../primitives/Button/Button'; +import Typography from '../../primitives/Typography/Typography'; +import Spinner from '../../primitives/Spinner/Spinner'; /** * Props interface for the BaseOrganizationList component. @@ -80,6 +84,10 @@ export interface BaseOrganizationListProps { * Custom renderer for each organization item */ renderOrganization?: (organization: OrganizationWithSwitchAccess, index: number) => ReactNode; + /** + * Function called when an organization is selected/clicked + */ + onOrganizationSelect?: (organization: OrganizationWithSwitchAccess) => void; /** * Inline styles to apply to the container */ @@ -104,12 +112,22 @@ export interface BaseOrganizationListProps { * Title for the popup dialog (only used in popup mode) */ title?: string; + /** + * Whether to show the organization status in the list + */ + showStatus?: boolean; } /** * Default organization item renderer */ -const defaultRenderOrganization = (organization: OrganizationWithSwitchAccess, styles: any): ReactNode => { +const defaultRenderOrganization = ( + organization: OrganizationWithSwitchAccess, + styles: any, + t: (key: string, params?: Record) => string, + onOrganizationSelect?: (organization: OrganizationWithSwitchAccess) => void, + showStatus?: boolean, +): ReactNode => { const getOrgInitials = (name?: string): string => { if (!name) return 'ORG'; return name @@ -121,30 +139,55 @@ const defaultRenderOrganization = (organization: OrganizationWithSwitchAccess, s }; return ( -
+
onOrganizationSelect(organization) : undefined} + >
- +
-

{organization.name}

-

@{organization.orgHandle}

-

- Status:{' '} - - {organization.status} - -

+ + {organization.name} + + + @{organization.orgHandle} + + {showStatus && ( + + {t('organization.switcher.status.label')}{' '} + + {organization.status} + + + )}
{organization.canSwitch ? ( - Can Switch + ) : ( - No Access + + {t('organization.switcher.no.access')} + )}
@@ -154,26 +197,43 @@ const defaultRenderOrganization = (organization: OrganizationWithSwitchAccess, s /** * Default loading renderer */ -const defaultRenderLoading = (styles: any): ReactNode => ( +const defaultRenderLoading = ( + t: (key: string, params?: Record) => string, + styles: any, +): ReactNode => (
-
Loading organizations...
+ + + {t('organization.switcher.loading.organizations')} +
); /** * Default error renderer */ -const defaultRenderError = (error: string, styles: any): ReactNode => ( +const defaultRenderError = ( + error: string, + t: (key: string, params?: Record) => string, + styles: any, +): ReactNode => (
- Error: {error} + + {t('organization.switcher.error.prefix')} {error} +
); /** * Default load more button renderer */ -const defaultRenderLoadMore = (onLoadMore: () => Promise, isLoading: boolean, styles: any): ReactNode => ( - + {isLoading ? t('organization.switcher.loading.more') : t('organization.switcher.load.more')} + ); /** * Default empty state renderer */ -const defaultRenderEmpty = (styles: any): ReactNode => ( +const defaultRenderEmpty = ( + t: (key: string, params?: Record) => string, + styles: any, +): ReactNode => (
-
No organizations found
+ + {t('organization.switcher.no.organizations')} +
); @@ -220,6 +286,7 @@ export const BaseOrganizationList: FC = ({ isLoadingMore = false, mode = 'inline', onOpenChange, + onOrganizationSelect, onRefresh, open = false, renderEmpty, @@ -230,18 +297,22 @@ export const BaseOrganizationList: FC = ({ style, title = 'Organizations', totalCount, + showStatus, }): ReactElement => { const styles = useStyles(); + const {t} = useTranslation(); - // Use custom renderers or defaults with styles - const renderLoadingWithStyles = renderLoading || (() => defaultRenderLoading(styles)); - const renderErrorWithStyles = renderError || ((error: string) => defaultRenderError(error, styles)); - const renderEmptyWithStyles = renderEmpty || (() => defaultRenderEmpty(styles)); + // Use custom renderers or defaults with styles and translations + const renderLoadingWithStyles = renderLoading || (() => defaultRenderLoading(t, styles)); + const renderErrorWithStyles = renderError || ((error: string) => defaultRenderError(error, t, styles)); + const renderEmptyWithStyles = renderEmpty || (() => defaultRenderEmpty(t, styles)); const renderLoadMoreWithStyles = renderLoadMore || - ((onLoadMore: () => Promise, isLoading: boolean) => defaultRenderLoadMore(onLoadMore, isLoading, styles)); + ((onLoadMore: () => Promise, isLoading: boolean) => defaultRenderLoadMore(onLoadMore, isLoading, t, styles)); const renderOrganizationWithStyles = - renderOrganization || ((org: OrganizationWithSwitchAccess) => defaultRenderOrganization(org, styles)); + renderOrganization || + ((org: OrganizationWithSwitchAccess) => + defaultRenderOrganization(org, styles, t, onOrganizationSelect, showStatus)); // Show loading state if (isLoading && data.length === 0) { @@ -323,17 +394,16 @@ export const BaseOrganizationList: FC = ({ {/* Header with total count and refresh button */}
-

Organizations

{totalCount !== undefined && ( -

- Showing {data.length} of {totalCount} organizations -

+ + {t('organization.switcher.showing.count', {showing: data.length, total: totalCount})} + )}
{onRefresh && ( - + )}
@@ -379,7 +449,6 @@ const useStyles = () => { margin: '0 auto', background: theme.colors.background.surface, borderRadius: theme.borderRadius.large, - boxShadow: theme.shadows.small, } as CSSProperties, header: { display: 'flex', @@ -482,10 +551,13 @@ const useStyles = () => { loadingContainer: { padding: `${theme.spacing.unit * 4}px`, textAlign: 'center' as const, + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + gap: `${theme.spacing.unit * 2}px`, } as CSSProperties, loadingText: { - color: theme.colors.text.secondary, - fontSize: '1rem', + marginTop: `${theme.spacing.unit}px`, } as CSSProperties, errorContainer: { backgroundColor: `${theme.colors.error.main}20`, diff --git a/packages/react/src/components/presentation/OrganizationList/OrganizationList.tsx b/packages/react/src/components/presentation/OrganizationList/OrganizationList.tsx index 9f7187405..9de37e776 100644 --- a/packages/react/src/components/presentation/OrganizationList/OrganizationList.tsx +++ b/packages/react/src/components/presentation/OrganizationList/OrganizationList.tsx @@ -134,75 +134,6 @@ export const OrganizationList: FC = ({ } }, [autoFetch, filter, limit, recursive, fetchPaginatedOrganizations]); - // Enhanced organization renderer that includes selection handler - const enhancedRenderOrganization = baseProps.renderOrganization - ? baseProps.renderOrganization - : onOrganizationSelect - ? (organization: OrganizationWithSwitchAccess, index: number) => ( -
onOrganizationSelect(organization)} - style={{ - border: '1px solid #e5e7eb', - borderRadius: '8px', - cursor: 'pointer', - display: 'flex', - justifyContent: 'space-between', - padding: '16px', - transition: 'all 0.2s', - }} - onMouseEnter={e => { - e.currentTarget.style.backgroundColor = '#f9fafb'; - e.currentTarget.style.borderColor = '#d1d5db'; - }} - onMouseLeave={e => { - e.currentTarget.style.backgroundColor = 'transparent'; - e.currentTarget.style.borderColor = '#e5e7eb'; - }} - > -
-

{organization.name}

-

Handle: {organization.orgHandle}

-

- Status:{' '} - - {organization.status} - -

-
-
- {organization.canSwitch ? ( - - Can Switch - - ) : ( - - No Access - - )} -
-
- ) - : undefined; - const refreshHandler = async () => { await fetchPaginatedOrganizations({ filter, @@ -220,8 +151,8 @@ export const OrganizationList: FC = ({ hasMore={hasMore} isLoading={isLoading} isLoadingMore={isLoadingMore} + onOrganizationSelect={onOrganizationSelect} onRefresh={refreshHandler} - renderOrganization={enhancedRenderOrganization} totalCount={totalCount} {...baseProps} /> diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx index b56f9027d..231f70737 100644 --- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx @@ -632,15 +632,14 @@ const BaseUserProfile: FC = ({ }; const getDisplayName = () => { - const currentUser = flattenedProfile || profile; - const firstName = getMappedUserProfileValue('firstName', mergedMappings, currentUser); - const lastName = getMappedUserProfileValue('lastName', mergedMappings, currentUser); + const firstName = getMappedUserProfileValue('firstName', mergedMappings, profile); + const lastName = getMappedUserProfileValue('lastName', mergedMappings, profile); if (firstName && lastName) { return `${firstName} ${lastName}`; } - return getMappedUserProfileValue('username', mergedMappings, currentUser) || ''; + return getMappedUserProfileValue('username', mergedMappings, profile) || ''; }; if (!profile && !flattenedProfile) { diff --git a/packages/react/src/contexts/Organization/OrganizationProvider.tsx b/packages/react/src/contexts/Organization/OrganizationProvider.tsx index 5eccc1b05..334aa53e2 100644 --- a/packages/react/src/contexts/Organization/OrganizationProvider.tsx +++ b/packages/react/src/contexts/Organization/OrganizationProvider.tsx @@ -253,22 +253,6 @@ const OrganizationProvider: FC> = ( limit, recursive, ...(reset ? {} : {startIndex: (currentPage - 1) * limit}), - fetcher: async (url: string, config: RequestInit): Promise => { - try { - const response = await fetch(url, config); - if (response.status === 401 || response.status === 403) { - const error = new Error('Insufficient permissions'); - (error as any).status = response.status; - throw error; - } - return response; - } catch (error: any) { - if (error.status === 401 || error.status === 403) { - error.noRetry = true; - } - throw error; - } - }, }); // Combine organization data with switch access information diff --git a/packages/react/src/contexts/Theme/ThemeProvider.tsx b/packages/react/src/contexts/Theme/ThemeProvider.tsx index 4176475ad..6880202ae 100644 --- a/packages/react/src/contexts/Theme/ThemeProvider.tsx +++ b/packages/react/src/contexts/Theme/ThemeProvider.tsx @@ -145,11 +145,11 @@ const ThemeProvider: FC> = ({ }); // Use branding theme if inheritFromBranding is enabled - const { - theme: brandingTheme, + const { + theme: brandingTheme, activeTheme: brandingActiveTheme, - isLoading: isBrandingLoading, - error: brandingError + isLoading: isBrandingLoading, + error: brandingError } = useBranding({ autoFetch: inheritFromBranding, // Don't pass forceTheme initially, let branding determine the active theme @@ -224,7 +224,7 @@ const ThemeProvider: FC> = ({ // Don't set up automatic theme detection for branding mode if (mode === 'branding') { - return; + return null; } if (mode === 'class') { From 1b6f159dbc8290c508a15b91292099aa44f2026c Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 2 Jul 2025 22:38:12 +0530 Subject: [PATCH 06/31] feat(javascript): add processUsername utility to clean userstore prefixes from usernames --- packages/javascript/src/api/getScim2Me.ts | 7 +- packages/javascript/src/index.ts | 1 + .../utils/__tests__/processUsername.test.ts | 178 ++++++++++++++++++ .../javascript/src/utils/processUsername.ts | 89 +++++++++ 4 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 packages/javascript/src/utils/__tests__/processUsername.test.ts create mode 100644 packages/javascript/src/utils/processUsername.ts diff --git a/packages/javascript/src/api/getScim2Me.ts b/packages/javascript/src/api/getScim2Me.ts index 5cba4f2e8..055881c51 100644 --- a/packages/javascript/src/api/getScim2Me.ts +++ b/packages/javascript/src/api/getScim2Me.ts @@ -18,6 +18,7 @@ import {User} from '../models/user'; import AsgardeoAPIError from '../errors/AsgardeoAPIError'; +import processUserUsername from '../utils/processUsername'; /** * Configuration for the getScim2Me request @@ -103,7 +104,7 @@ const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeC } const fetchFn = fetcher || fetch; - const resolvedUrl: string = url ?? `${baseUrl}/scim2/Me` + const resolvedUrl: string = url ?? `${baseUrl}/scim2/Me`; const requestInit: RequestInit = { ...requestConfig, @@ -130,7 +131,9 @@ const getScim2Me = async ({url, baseUrl, fetcher, ...requestConfig}: GetScim2MeC ); } - return (await response.json()) as User; + const user = (await response.json()) as User; + + return processUserUsername(user); } catch (error) { if (error instanceof AsgardeoAPIError) { throw error; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index a20b6f9c0..5b891ec77 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -115,6 +115,7 @@ export {default as AsgardeoJavaScriptClient} from './AsgardeoJavaScriptClient'; export {default as createTheme} from './theme/createTheme'; export {ThemeColors, ThemeConfig, Theme, ThemeMode, ThemeDetection} from './theme/types'; +export {default as processUsername} from './utils/processUsername'; export {default as deepMerge} from './utils/deepMerge'; export {default as deriveOrganizationHandleFromBaseUrl} from './utils/deriveOrganizationHandleFromBaseUrl'; export {default as extractUserClaimsFromIdToken} from './utils/extractUserClaimsFromIdToken'; diff --git a/packages/javascript/src/utils/__tests__/processUsername.test.ts b/packages/javascript/src/utils/__tests__/processUsername.test.ts new file mode 100644 index 000000000..aafb12feb --- /dev/null +++ b/packages/javascript/src/utils/__tests__/processUsername.test.ts @@ -0,0 +1,178 @@ +/** + * 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. + */ + +import processUsername, {removeUserstorePrefixp} from '../processUsername'; + +describe('processUsername', () => { + describe('removeUserstorePrefix', () => { + it('should remove DEFAULT/ prefix from username', () => { + const result = removeUserstorePrefix('DEFAULT/john.doe'); + expect(result).toBe('john.doe'); + }); + + it('should remove ASGARDEO_USER/ prefix from username', () => { + const result = removeUserstorePrefix('ASGARDEO_USER/jane.doe'); + expect(result).toBe('jane.doe'); + }); + + it('should remove PRIMARY/ prefix from username', () => { + const result = removeUserstorePrefix('PRIMARY/admin'); + expect(result).toBe('admin'); + }); + + it('should remove custom userstore prefix from username', () => { + const result = removeUserstorePrefix('CUSTOM_STORE/user.name'); + expect(result).toBe('user.name'); + }); + + it('should return original username if no userstore prefix exists', () => { + const result = removeUserstorePrefix('jane.doe'); + expect(result).toBe('jane.doe'); + }); + + it('should handle empty string', () => { + const result = removeUserstorePrefix(''); + expect(result).toBe(''); + }); + + it('should handle undefined input', () => { + const result = removeUserstorePrefix(undefined); + expect(result).toBe(''); + }); + + it('should handle username with only userstore prefix', () => { + const result = removeUserstorePrefix('DEFAULT/'); + expect(result).toBe(''); + }); + + it('should not remove lowercase prefixes', () => { + const result = removeUserstorePrefix('default/user'); + expect(result).toBe('default/user'); + }); + + it('should not remove mixed case prefixes', () => { + const result = removeUserstorePrefix('Default/user'); + expect(result).toBe('Default/user'); + }); + + it('should not remove if prefix contains invalid characters', () => { + const result = removeUserstorePrefix('DEFAULT-STORE/user'); + expect(result).toBe('DEFAULT-STORE/user'); + }); + + it('should only remove the first occurrence of userstore prefix', () => { + const result = removeUserstorePrefix('DEFAULT/DEFAULT/user'); + expect(result).toBe('DEFAULT/user'); + }); + + it('should handle userstore prefix with numbers', () => { + const result = removeUserstorePrefix('STORE123/user'); + expect(result).toBe('user'); + }); + }); + + describe('processUserUsername', () => { + it('should process DEFAULT/ username in user object', () => { + const user = { + username: 'DEFAULT/john.doe', + email: 'john@example.com', + givenName: 'John', + }; + + const result = processUserUsername(user); + + expect(result.username).toBe('john.doe'); + expect(result.email).toBe('john@example.com'); + expect(result.givenName).toBe('John'); + }); + + it('should process ASGARDEO_USER/ username in user object', () => { + const user = { + username: 'ASGARDEO_USER/jane.doe', + email: 'jane@example.com', + givenName: 'Jane', + }; + + const result = processUserUsername(user); + + expect(result.username).toBe('jane.doe'); + expect(result.email).toBe('jane@example.com'); + expect(result.givenName).toBe('Jane'); + }); + + it('should process PRIMARY/ username in user object', () => { + const user = { + username: 'PRIMARY/admin', + email: 'admin@example.com', + givenName: 'Admin', + }; + + const result = processUserUsername(user); + + expect(result.username).toBe('admin'); + expect(result.email).toBe('admin@example.com'); + expect(result.givenName).toBe('Admin'); + }); + + it('should handle user object without username', () => { + const user = { + email: 'john@example.com', + givenName: 'John', + }; + + const result = processUserUsername(user); + + expect(result).toEqual(user); + }); + + it('should handle user object with empty username', () => { + const user = { + username: '', + email: 'john@example.com', + }; + + const result = processUserUsername(user); + + expect(result.username).toBe(''); + expect(result.email).toBe('john@example.com'); + }); + + it('should handle null/undefined user object', () => { + expect(processUserUsername(null as any)).toBe(null); + expect(processUserUsername(undefined as any)).toBe(undefined); + }); + + it('should preserve other properties in user object', () => { + const user = { + username: 'DEFAULT/jane.doe', + email: 'jane@example.com', + givenName: 'Jane', + familyName: 'Doe', + customProperty: 'customValue', + }; + + const result = processUserUsername(user); + + expect(result.username).toBe('jane.doe'); + expect(result.email).toBe('jane@example.com'); + expect(result.givenName).toBe('Jane'); + expect(result.familyName).toBe('Doe'); + expect((result as any).customProperty).toBe('customValue'); + }); + }); +}); diff --git a/packages/javascript/src/utils/processUsername.ts b/packages/javascript/src/utils/processUsername.ts new file mode 100644 index 000000000..315210add --- /dev/null +++ b/packages/javascript/src/utils/processUsername.ts @@ -0,0 +1,89 @@ +/** + * 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. + */ + +/** + * Regular expression to match userstore prefixes in usernames. + * Matches patterns like "DEFAULT/", "ASGARDEO_USER/", "PRIMARY/", etc. + * The pattern matches any uppercase letters, numbers, and underscores followed by a forward slash. + */ +const USERSTORE_PREFIX_REGEX = /^[A-Z_][A-Z0-9_]*\//; + +/** + * Removes userstore prefixes from a username if they exist. + * This is commonly used to clean usernames returned from SCIM2 endpoints + * that include userstore prefixes like "DEFAULT/", "ASGARDEO_USER/", "PRIMARY/", etc. + * + * @param username - The username string to process + * @returns The username without the userstore prefix, or the original username if no prefix exists + * + * @example + * ```typescript + * const cleanUsername = removeUserstorePrefix("DEFAULT/john.doe"); + * console.log(cleanUsername); // "john.doe" + * + * const asgardeoUser = removeUserstorePrefix("ASGARDEO_USER/jane.doe"); + * console.log(asgardeoUser); // "jane.doe" + * + * const primaryUser = removeUserstorePrefix("PRIMARY/admin"); + * console.log(primaryUser); // "admin" + * + * const alreadyClean = removeUserstorePrefix("user.name"); + * console.log(alreadyClean); // "user.name" + * + * const emptyInput = removeUserstorePrefix(""); + * console.log(emptyInput); // "" + * ``` + */ +export const removeUserstorePrefix = (username?: string): string => { + if (!username) { + return ''; + } + + return username.replace(USERSTORE_PREFIX_REGEX, ''); +}; + +/** + * Processes a user object to remove userstore prefixes from the username field. + * This is a helper function for processing user objects returned from SCIM2 endpoints. + * + * @param user - The user object to process + * @returns The user object with the processed username + * + * @example + * ```typescript + * const user = { username: "DEFAULT/john.doe", email: "john@example.com" }; + * const processedUser = processUserUsername(user); + * console.log(processedUser.username); // "john.doe" + * + * const asgardeoUser = { username: "ASGARDEO_USER/jane.doe", email: "jane@example.com" }; + * const processedAsgardeoUser = processUserUsername(asgardeoUser); + * console.log(processedAsgardeoUser.username); // "jane.doe" + * ``` + */ +const processUsername = (user: T): T => { + if (!user || !user.username) { + return user; + } + + return { + ...user, + username: removeUserstorePrefix(user.username), + }; +}; + +export default processUsername; From 8b440d0f14c1e709f8395715e569723b58afa957 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 2 Jul 2025 22:38:45 +0530 Subject: [PATCH 07/31] fix(react): adjust spacing and remove border from list items for improved layout --- .../react/src/components/primitives/MultiInput/MultiInput.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/components/primitives/MultiInput/MultiInput.tsx b/packages/react/src/components/primitives/MultiInput/MultiInput.tsx index 3808e2776..120518e57 100644 --- a/packages/react/src/components/primitives/MultiInput/MultiInput.tsx +++ b/packages/react/src/components/primitives/MultiInput/MultiInput.tsx @@ -122,7 +122,7 @@ const useStyles = () => { listContainer: { display: 'flex', flexDirection: 'column' as const, - gap: `${theme.spacing.unit / 2}px`, + gap: `${theme.spacing.unit * 0}px`, }, listItem: { display: 'flex', @@ -130,7 +130,6 @@ const useStyles = () => { justifyContent: 'space-between', padding: `${theme.spacing.unit}px ${theme.spacing.unit * 1.5}px`, backgroundColor: theme.colors.background.surface, - border: `1px solid ${theme.colors.border}`, borderRadius: theme.borderRadius.medium, fontSize: '1rem', color: theme.colors.text.primary, From 79f196de3028f19ebbda752f1471050bc1ba6a72 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 03:35:27 +0530 Subject: [PATCH 08/31] refactor: Update organization management in Asgardeo integration - Renamed context property from `organizations` to `myOrganizations` in OrganizationSwitcher and related components. - Introduced new server actions `getAllOrganizations` and `getMyOrganizations` for fetching organization data. - Updated AsgardeoProvider to manage and provide `myOrganizations` and `getAllOrganizations` to the context. - Refactored OrganizationProvider to utilize new organization fetching methods and removed legacy pagination logic. - Enhanced BaseOrganizationList to handle both all organizations and user-specific organizations with switch access. - Updated CreateOrganization component to refresh the list of user organizations after creation. - Adjusted OrganizationList to fetch all organizations on mount and pass necessary props to BaseOrganizationList. - Cleaned up unused code and improved error handling across organization-related components. --- .../src/AsgardeoJavaScriptClient.ts | 5 +- .../javascript/src/api/getAllOrganizations.ts | 14 +- packages/javascript/src/index.ts | 7 +- packages/javascript/src/models/client.ts | 19 +- .../javascript/src/models/organization.ts | 10 + packages/nextjs/src/AsgardeoNextClient.ts | 56 +++- .../CreateOrganization/CreateOrganization.tsx | 4 +- .../OrganizationList/OrganizationList.tsx | 123 ++------- .../OrganizationSwitcher.tsx | 2 +- .../contexts/Asgardeo/AsgardeoProvider.tsx | 16 +- .../nextjs/src/server/AsgardeoProvider.tsx | 16 +- .../src/server/actions/getAllOrganizations.ts | 41 +++ ...zationsAction.ts => getMyOrganizations.ts} | 22 +- packages/react/src/AsgardeoReactClient.ts | 32 ++- packages/react/src/api/getAllOrganizations.ts | 4 +- .../CreateOrganization/CreateOrganization.tsx | 4 +- .../OrganizationList/BaseOrganizationList.tsx | 89 +++---- .../OrganizationList/OrganizationList.tsx | 64 ++--- .../OrganizationSwitcher.tsx | 2 +- .../contexts/Asgardeo/AsgardeoProvider.tsx | 12 +- .../Organization/OrganizationContext.ts | 67 +---- .../Organization/OrganizationProvider.tsx | 242 ++---------------- .../contexts/Organization/useOrganization.ts | 4 +- 23 files changed, 314 insertions(+), 541 deletions(-) create mode 100644 packages/nextjs/src/server/actions/getAllOrganizations.ts rename packages/nextjs/src/server/actions/{getOrganizationsAction.ts => getMyOrganizations.ts} (60%) diff --git a/packages/javascript/src/AsgardeoJavaScriptClient.ts b/packages/javascript/src/AsgardeoJavaScriptClient.ts index 0b300faa5..3085cadb7 100644 --- a/packages/javascript/src/AsgardeoJavaScriptClient.ts +++ b/packages/javascript/src/AsgardeoJavaScriptClient.ts @@ -16,6 +16,7 @@ * under the License. */ +import {AllOrganizationsApiResponse} from './models/organization'; import {AsgardeoClient, SignInOptions, SignOutOptions, SignUpOptions} from './models/client'; import {Config} from './models/config'; import {EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse} from './models/embedded-flow'; @@ -36,7 +37,9 @@ abstract class AsgardeoJavaScriptClient implements AsgardeoClient abstract getUser(options?: any): Promise; - abstract getOrganizations(options?: any): Promise; + abstract getAllOrganizations(options?: any, sessionId?: string): Promise; + + abstract getMyOrganizations(options?: any, sessionId?: string): Promise; abstract getCurrentOrganization(sessionId?: string): Promise; diff --git a/packages/javascript/src/api/getAllOrganizations.ts b/packages/javascript/src/api/getAllOrganizations.ts index a755efbb4..4c4e02234 100644 --- a/packages/javascript/src/api/getAllOrganizations.ts +++ b/packages/javascript/src/api/getAllOrganizations.ts @@ -16,19 +16,9 @@ * under the License. */ -import {Organization} from '../models/organization'; +import {AllOrganizationsApiResponse} from '../models/organization'; import AsgardeoAPIError from '../errors/AsgardeoAPIError'; -/** - * Interface for paginated organization response. - */ -export interface PaginatedOrganizationsResponse { - hasMore?: boolean; - nextCursor?: string; - organizations: Organization[]; - totalCount?: number; -} - /** * Configuration for the getAllOrganizations request */ @@ -120,7 +110,7 @@ const getAllOrganizations = async ({ recursive = false, fetcher, ...requestConfig -}: GetAllOrganizationsConfig): Promise => { +}: GetAllOrganizationsConfig): Promise => { try { new URL(baseUrl); } catch (error) { diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 5b891ec77..09bd1fb5b 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -27,11 +27,7 @@ export {default as executeEmbeddedSignUpFlow} from './api/executeEmbeddedSignUpF export {default as getUserInfo} from './api/getUserInfo'; export {default as getScim2Me, GetScim2MeConfig} from './api/getScim2Me'; export {default as getSchemas, GetSchemasConfig} from './api/getSchemas'; -export { - default as getAllOrganizations, - PaginatedOrganizationsResponse, - GetAllOrganizationsConfig, -} from './api/getAllOrganizations'; +export {default as getAllOrganizations, GetAllOrganizationsConfig} from './api/getAllOrganizations'; export { default as createOrganization, CreateOrganizationPayload, @@ -53,6 +49,7 @@ export {default as AsgardeoAPIError} from './errors/AsgardeoAPIError'; export {default as AsgardeoRuntimeError} from './errors/AsgardeoRuntimeError'; export {AsgardeoAuthException} from './errors/exception'; +export {AllOrganizationsApiResponse} from './models/organization'; export { EmbeddedSignInFlowInitiateResponse, EmbeddedSignInFlowStatus, diff --git a/packages/javascript/src/models/client.ts b/packages/javascript/src/models/client.ts index d78a73012..82dfb128e 100644 --- a/packages/javascript/src/models/client.ts +++ b/packages/javascript/src/models/client.ts @@ -16,7 +16,12 @@ * under the License. */ -import {EmbeddedFlowExecuteRequestConfig, EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse} from './embedded-flow'; +import {AllOrganizationsApiResponse} from '../models/organization'; +import { + EmbeddedFlowExecuteRequestConfig, + EmbeddedFlowExecuteRequestPayload, + EmbeddedFlowExecuteResponse, +} from './embedded-flow'; import {EmbeddedSignInFlowHandleRequestPayload} from './embedded-signin-flow'; import {Organization} from './organization'; import {User, UserProfile} from './user'; @@ -37,11 +42,13 @@ export type SignUpOptions = Record; */ export interface AsgardeoClient { /** - * Gets the users associated organizations. + * Gets the current signed-in user's associated organizations. * * @returns Associated organizations. */ - getOrganizations(options?: any): Promise; + getMyOrganizations(options?: any, sessionId?: string): Promise; + + getAllOrganizations(options?: any, sessionId?: string): Promise; /** * Gets the current organization of the user. @@ -147,7 +154,11 @@ export interface AsgardeoClient { * @param afterSignOut - Callback function to be executed after sign-out is complete. * @returns A promise that resolves to true if sign-out is successful */ - signOut(options?: SignOutOptions, sessionId?: string, afterSignOut?: (afterSignOutUrl: string) => void): Promise; + signOut( + options?: SignOutOptions, + sessionId?: string, + afterSignOut?: (afterSignOutUrl: string) => void, + ): Promise; /** * Initiates a redirection-based sign-up process for the user. diff --git a/packages/javascript/src/models/organization.ts b/packages/javascript/src/models/organization.ts index b19a4ac99..fd166ca3d 100644 --- a/packages/javascript/src/models/organization.ts +++ b/packages/javascript/src/models/organization.ts @@ -23,3 +23,13 @@ export interface Organization { ref?: string; status?: string; } + +/** + * Interface for paginated organization response. + */ +export interface AllOrganizationsApiResponse { + hasMore?: boolean; + nextCursor?: string; + organizations: Organization[]; + totalCount?: number; +} diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index d39d22064..ae6015439 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -46,7 +46,9 @@ import { CreateOrganizationPayload, getOrganization, OrganizationDetails, - deriveOrganizationHandleFromBaseUrl + deriveOrganizationHandleFromBaseUrl, + getAllOrganizations, + AllOrganizationsApiResponse, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; @@ -101,8 +103,17 @@ class AsgardeoNextClient exte return Promise.resolve(true); } - const {baseUrl, organizationHandle, clientId, clientSecret, signInUrl, afterSignInUrl, afterSignOutUrl, signUpUrl, ...rest} = - decorateConfigWithNextEnv(config); + const { + baseUrl, + organizationHandle, + clientId, + clientSecret, + signInUrl, + afterSignInUrl, + afterSignOutUrl, + signUpUrl, + ...rest + } = decorateConfigWithNextEnv(config); this.isInitialized = true; @@ -263,25 +274,46 @@ class AsgardeoNextClient exte } } - override async getOrganizations(userId?: string): Promise { + override async getMyOrganizations(options?: any, userId?: string): Promise { try { const configData = await this.asgardeo.getConfigData(); const baseUrl: string = configData?.baseUrl as string; - const organizations = await getMeOrganizations({ + return await getMeOrganizations({ baseUrl, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, }, }); + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to fetch the user's associated organizations: ${ + error instanceof Error ? error.message : String(error) + }`, + 'AsgardeoNextClient-getMyOrganizations-RuntimeError-001', + 'nextjs', + 'An error occurred while fetching associated organizations of the signed-in user.', + ); + } + } - return organizations; + override async getAllOrganizations(options?: any, userId?: string): Promise { + try { + const configData = await this.asgardeo.getConfigData(); + const baseUrl: string = configData?.baseUrl as string; + + return getAllOrganizations({ + baseUrl, + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + }); } catch (error) { throw new AsgardeoRuntimeError( - 'Failed to fetch organizations.', - 'react-AsgardeoReactClient-GetOrganizationsError-001', - 'react', - 'An error occurred while fetching the organizations associated with the user.', + `Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`, + 'AsgardeoNextClient-getAllOrganizations-RuntimeError-001', + 'nextjs', + 'An error occurred while fetching all the organizations associated with the user.', ); } } @@ -304,8 +336,8 @@ class AsgardeoNextClient exte if (!organization.id) { throw new AsgardeoRuntimeError( 'Organization ID is required for switching organizations', - 'react-AsgardeoReactClient-ValidationError-001', - 'react', + 'AsgardeoNextClient-switchOrganization-ValidationError-001', + 'nextjs', 'The organization object must contain a valid ID to perform the organization switch.', ); } diff --git a/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx b/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx index b111912c4..08899b8cf 100644 --- a/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx +++ b/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx @@ -80,7 +80,7 @@ export const CreateOrganization: FC = ({ ...props }: CreateOrganizationProps): ReactElement => { const {isSignedIn, baseUrl} = useAsgardeo(); - const {currentOrganization, revalidateOrganizations} = useOrganization(); + const {currentOrganization, revalidateMyOrganizations} = useOrganization(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -121,7 +121,7 @@ export const CreateOrganization: FC = ({ } // Refresh organizations list to include the new organization - await revalidateOrganizations(); + await revalidateMyOrganizations(); // Call success callback if provided if (onSuccess) { diff --git a/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx b/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx index 0ab5d680c..ff0288597 100644 --- a/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx +++ b/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx @@ -18,8 +18,8 @@ 'use client'; -import {withVendorCSSClassPrefix} from '@asgardeo/node'; -import {FC, ReactElement, useEffect, useMemo, CSSProperties} from 'react'; +import {AllOrganizationsApiResponse, withVendorCSSClassPrefix} from '@asgardeo/node'; +import {FC, ReactElement, useEffect, useMemo, CSSProperties, useState} from 'react'; import { BaseOrganizationListProps, BaseOrganizationList, @@ -56,7 +56,7 @@ export interface OrganizationListConfig { export interface OrganizationListProps extends Omit< BaseOrganizationListProps, - 'data' | 'error' | 'fetchMore' | 'hasMore' | 'isLoading' | 'isLoadingMore' | 'totalCount' + 'allOrganizations' | 'error' | 'fetchMore' | 'hasMore' | 'isLoading' | 'isLoadingMore' | 'myOrganizations' >, OrganizationListConfig { /** @@ -113,118 +113,25 @@ export const OrganizationList: FC = ({ recursive = false, ...baseProps }: OrganizationListProps): ReactElement => { - const { - paginatedOrganizations, - error, - fetchMore, - hasMore, - isLoading, - isLoadingMore, - totalCount, - fetchPaginatedOrganizations, - } = useOrganization(); + const {getAllOrganizations, error, isLoading, myOrganizations} = useOrganization(); - // Auto-fetch organizations on mount or when parameters change - useEffect(() => { - if (autoFetch) { - fetchPaginatedOrganizations({ - filter, - limit, - recursive, - reset: true, - }); - } - }, [autoFetch, filter, limit, recursive, fetchPaginatedOrganizations]); - - // Enhanced organization renderer that includes selection handler - const enhancedRenderOrganization = baseProps.renderOrganization - ? baseProps.renderOrganization - : onOrganizationSelect - ? (organization: OrganizationWithSwitchAccess, index: number) => ( -
onOrganizationSelect(organization)} - style={{ - border: '1px solid #e5e7eb', - borderRadius: '8px', - cursor: 'pointer', - display: 'flex', - justifyContent: 'space-between', - padding: '16px', - transition: 'all 0.2s', - }} - onMouseEnter={e => { - e.currentTarget.style.backgroundColor = '#f9fafb'; - e.currentTarget.style.borderColor = '#d1d5db'; - }} - onMouseLeave={e => { - e.currentTarget.style.backgroundColor = 'transparent'; - e.currentTarget.style.borderColor = '#e5e7eb'; - }} - > -
-

{organization.name}

-

Handle: {organization.orgHandle}

-

- Status:{' '} - - {organization.status} - -

-
-
- {organization.canSwitch ? ( - - Can Switch - - ) : ( - - No Access - - )} -
-
- ) - : undefined; + const [allOrganizations, setAllOrganizations] = useState({ + organizations: [], + }); - const refreshHandler = async () => { - await fetchPaginatedOrganizations({ - filter, - limit, - recursive, - reset: true, - }); - }; + useEffect(() => { + (async () => { + setAllOrganizations(await getAllOrganizations()); + })(); + }, []); return ( ); diff --git a/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx index b840e0052..22945cf77 100644 --- a/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -93,7 +93,7 @@ export const OrganizationSwitcher: FC = ({ const {isSignedIn} = useAsgardeo(); const { currentOrganization: contextCurrentOrganization, - organizations: contextOrganizations, + myOrganizations: contextOrganizations, switchOrganization, isLoading, error, diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index e03cb254b..f046908ec 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -19,6 +19,7 @@ 'use client'; import { + AllOrganizationsApiResponse, AsgardeoRuntimeError, EmbeddedFlowExecuteRequestConfig, EmbeddedFlowExecuteRequestPayload, @@ -40,7 +41,6 @@ import { 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'; @@ -67,6 +67,9 @@ export type AsgardeoClientProviderProps = Partial Promise<{success: boolean; data: {user: User}; error: string}>; + getAllOrganizations: (options?: any, sessionId?: string) => Promise; + myOrganizations: Organization[]; + revalidateMyOrganizations?: (sessionId?: string) => Promise; }; const AsgardeoClientProvider: FC> = ({ @@ -86,6 +89,9 @@ const AsgardeoClientProvider: FC> updateProfile, applicationId, organizationHandle, + myOrganizations, + revalidateMyOrganizations, + getAllOrganizations, }: PropsWithChildren) => { const reRenderCheckRef: RefObject = useRef(false); const router = useRouter(); @@ -305,13 +311,11 @@ const AsgardeoClientProvider: FC> { - const result = await getOrganizationsAction((await getSessionId()) as string); - - return result?.data?.organizations || []; - }} + getAllOrganizations={getAllOrganizations} + myOrganizations={myOrganizations} currentOrganization={currentOrganization} onOrganizationSwitch={switchOrganization} + revalidateMyOrganizations={revalidateMyOrganizations as any} > {children} diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index dea32dc13..309998ee6 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -19,7 +19,7 @@ 'use server'; import {FC, PropsWithChildren, ReactElement} from 'react'; -import {AsgardeoRuntimeError, Organization, User, UserProfile} from '@asgardeo/node'; +import {AllOrganizationsApiResponse, AsgardeoRuntimeError, Organization, User, UserProfile} from '@asgardeo/node'; import AsgardeoClientProvider from '../client/contexts/Asgardeo/AsgardeoProvider'; import AsgardeoNextClient from '../AsgardeoNextClient'; import signInAction from './actions/signInAction'; @@ -34,6 +34,8 @@ import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction'; import {AsgardeoProviderProps} from '@asgardeo/react'; import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction'; import updateUserProfileAction from './actions/updateUserProfileAction'; +import getMyOrganizations from './actions/getMyOrganizations'; +import getAllOrganizations from './actions/getAllOrganizations'; /** * Props interface of {@link AsgardeoServerProvider} @@ -97,17 +99,27 @@ const AsgardeoServerProvider: FC> name: '', orgHandle: '', }; + let myOrganizations: Organization[] = []; if (_isSignedIn) { const userResponse = await getUserAction(sessionId); const userProfileResponse = await getUserProfileAction(sessionId); const currentOrganizationResponse = await getCurrentOrganizationAction(sessionId); + myOrganizations = await getMyOrganizations({}, sessionId); user = userResponse.data?.user || {}; userProfile = userProfileResponse.data?.userProfile; currentOrganization = currentOrganizationResponse?.data?.organization as Organization; } + const handleGetAllOrganizations = async ( + options?: any, + _sessionId?: string, + ): Promise => { + 'use server'; + return await getAllOrganizations(options, sessionId); + }; + return ( > userProfile={userProfile} updateProfile={updateUserProfileAction} isSignedIn={_isSignedIn} + myOrganizations={myOrganizations} + getAllOrganizations={handleGetAllOrganizations} > {children} diff --git a/packages/nextjs/src/server/actions/getAllOrganizations.ts b/packages/nextjs/src/server/actions/getAllOrganizations.ts new file mode 100644 index 000000000..9902f8bf8 --- /dev/null +++ b/packages/nextjs/src/server/actions/getAllOrganizations.ts @@ -0,0 +1,41 @@ +/** + * 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 {AllOrganizationsApiResponse, AsgardeoAPIError, Organization} from '@asgardeo/node'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; + +/** + * Server action to get organizations. + */ +const getAllOrganizations = async (options?: any, sessionId?: string | undefined): Promise => { + try { + const client = AsgardeoNextClient.getInstance(); + return client.getAllOrganizations(options, sessionId); + } catch (error) { + throw new AsgardeoAPIError( + `Failed to get all the organizations for the user: ${error instanceof Error ? error.message : String(error)}`, + 'getAllOrganizations-ServerActionError-001', + 'nextjs', + error instanceof AsgardeoAPIError ? error.statusCode : undefined, + ); + } +}; + +export default getAllOrganizations; diff --git a/packages/nextjs/src/server/actions/getOrganizationsAction.ts b/packages/nextjs/src/server/actions/getMyOrganizations.ts similarity index 60% rename from packages/nextjs/src/server/actions/getOrganizationsAction.ts rename to packages/nextjs/src/server/actions/getMyOrganizations.ts index 16c878071..d63aa7115 100644 --- a/packages/nextjs/src/server/actions/getOrganizationsAction.ts +++ b/packages/nextjs/src/server/actions/getMyOrganizations.ts @@ -18,26 +18,24 @@ 'use server'; -import {Organization} from '@asgardeo/node'; +import {AsgardeoAPIError, Organization} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; /** * Server action to get organizations. */ -const getOrganizationsAction = async (sessionId: string) => { +const getMyOrganizations = async (options?: any, sessionId?: string | undefined): Promise => { try { const client = AsgardeoNextClient.getInstance(); - const organizations: Organization[] = await client.getOrganizations(sessionId); - return {success: true, data: {organizations}, error: null}; + return await client.getMyOrganizations(options, sessionId); } catch (error) { - return { - success: false, - data: { - user: {}, - }, - error: 'Failed to get organizations', - }; + throw new AsgardeoAPIError( + `Failed to get the organizations for the user: ${error instanceof Error ? error.message : String(error)}`, + 'getMyOrganizations-ServerActionError-001', + 'nextjs', + error instanceof AsgardeoAPIError ? error.statusCode : undefined, + ); } }; -export default getOrganizationsAction; +export default getMyOrganizations; diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 86f1b2e9b..98c5f9356 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -36,12 +36,14 @@ import { IdToken, EmbeddedFlowExecuteRequestConfig, deriveOrganizationHandleFromBaseUrl, + AllOrganizationsApiResponse, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import getMeOrganizations from './api/getMeOrganizations'; import getScim2Me from './api/getScim2Me'; import getSchemas from './api/getSchemas'; import {AsgardeoReactConfig} from './models/config'; +import getAllOrganizations from './api/getAllOrganizations'; /** * Client for mplementing Asgardeo in React applications. @@ -125,7 +127,7 @@ class AsgardeoReactClient e } } - override async getOrganizations(options?: any): Promise { + override async getMyOrganizations(options?: any, sessionId?: string): Promise { try { let baseUrl = options?.baseUrl; @@ -134,15 +136,33 @@ class AsgardeoReactClient e baseUrl = configData?.baseUrl; } - const organizations = await getMeOrganizations({baseUrl}); + return getMeOrganizations({baseUrl}); + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to fetch the user's associated organizations: ${error instanceof Error ? error.message : String(error)}`, + 'AsgardeoReactClient-getMyOrganizations-RuntimeError-001', + 'react', + 'An error occurred while fetching associated organizations of the signed-in user.', + ); + } + } + + override async getAllOrganizations(options?: any, sessionId?: string): Promise { + try { + let baseUrl = options?.baseUrl; + + if (!baseUrl) { + const configData = await this.asgardeo.getConfigData(); + baseUrl = configData?.baseUrl; + } - return organizations; + return getAllOrganizations({baseUrl}); } catch (error) { throw new AsgardeoRuntimeError( - 'Failed to fetch organizations.', - 'react-AsgardeoReactClient-GetOrganizationsError-001', + `Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`, + 'AsgardeoReactClient-getAllOrganizations-RuntimeError-001', 'react', - 'An error occurred while fetching the organizations associated with the user.', + 'An error occurred while fetching all the organizations associated with the user.', ); } } diff --git a/packages/react/src/api/getAllOrganizations.ts b/packages/react/src/api/getAllOrganizations.ts index 452755fea..48f79aa7a 100644 --- a/packages/react/src/api/getAllOrganizations.ts +++ b/packages/react/src/api/getAllOrganizations.ts @@ -22,7 +22,7 @@ import { HttpRequestConfig, getAllOrganizations as baseGetAllOrganizations, GetAllOrganizationsConfig as BaseGetAllOrganizationsConfig, - PaginatedOrganizationsResponse, + AllOrganizationsApiResponse, } from '@asgardeo/browser'; const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); @@ -84,7 +84,7 @@ export interface GetAllOrganizationsConfig extends Omit => { +}: GetAllOrganizationsConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { const response = await httpClient({ url, diff --git a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx index a6f9aca8d..3a833db62 100644 --- a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx @@ -78,7 +78,7 @@ export const CreateOrganization: FC = ({ ...props }: CreateOrganizationProps): ReactElement => { const {isSignedIn, baseUrl} = useAsgardeo(); - const {currentOrganization, revalidateOrganizations} = useOrganization(); + const {currentOrganization, revalidateMyOrganizations} = useOrganization(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -119,7 +119,7 @@ export const CreateOrganization: FC = ({ } // Refresh organizations list to include the new organization - await revalidateOrganizations(); + await revalidateMyOrganizations(); // Call success callback if provided if (onSuccess) { diff --git a/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx b/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx index 832da5e74..785d4001e 100644 --- a/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx +++ b/packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx @@ -16,10 +16,9 @@ * under the License. */ -import {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import {AllOrganizationsApiResponse, Organization, withVendorCSSClassPrefix} from '@asgardeo/browser'; import clsx from 'clsx'; import {FC, ReactElement, ReactNode, useMemo, CSSProperties} from 'react'; -import {OrganizationWithSwitchAccess} from '../../../contexts/Organization/OrganizationContext'; import useTheme from '../../../contexts/Theme/useTheme'; import useTranslation from '../../../hooks/useTranslation'; import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; @@ -28,6 +27,10 @@ import Button from '../../primitives/Button/Button'; import Typography from '../../primitives/Typography/Typography'; import Spinner from '../../primitives/Spinner/Spinner'; +export interface OrganizationWithSwitchAccess extends Organization { + canSwitch: boolean; +} + /** * Props interface for the BaseOrganizationList component. */ @@ -37,9 +40,13 @@ export interface BaseOrganizationListProps { */ className?: string; /** - * List of organizations with switch access information + * List of organizations discoverable to the signed-in user. + */ + allOrganizations: AllOrganizationsApiResponse; + /** + * List of organizations associated to the signed-in user. */ - data: OrganizationWithSwitchAccess[]; + myOrganizations: Organization[]; /** * Error message to display */ @@ -92,10 +99,6 @@ export interface BaseOrganizationListProps { * Inline styles to apply to the container */ style?: React.CSSProperties; - /** - * Total number of organizations - */ - totalCount?: number; /** * Display mode: 'inline' for normal display, 'popup' for modal dialog */ @@ -128,24 +131,12 @@ const defaultRenderOrganization = ( onOrganizationSelect?: (organization: OrganizationWithSwitchAccess) => void, showStatus?: boolean, ): ReactNode => { - const getOrgInitials = (name?: string): string => { - if (!name) return 'ORG'; - return name - .split(' ') - .map(word => word.charAt(0)) - .join('') - .toUpperCase() - .slice(0, 2); - }; - return (
onOrganizationSelect(organization) : undefined} >
@@ -171,25 +162,20 @@ const defaultRenderOrganization = ( )}
-
- {organization.canSwitch ? ( + {organization.canSwitch && ( +
- ) : ( - - {t('organization.switcher.no.access')} - - )} -
+
+ )}
); }; @@ -278,7 +264,8 @@ const defaultRenderEmpty = ( */ export const BaseOrganizationList: FC = ({ className = '', - data, + allOrganizations, + myOrganizations, error, fetchMore, hasMore = false, @@ -296,12 +283,26 @@ export const BaseOrganizationList: FC = ({ renderOrganization, style, title = 'Organizations', - totalCount, showStatus, }): ReactElement => { const styles = useStyles(); const {t} = useTranslation(); + // Combine allOrganizations with myOrganizations to determine which orgs can be switched to + const organizationsWithSwitchAccess: OrganizationWithSwitchAccess[] = useMemo(() => { + if (!allOrganizations?.organizations) { + return []; + } + + // Create a Set of IDs from myOrganizations for faster lookup + const myOrgIds = new Set(myOrganizations?.map(org => org.id) || []); + + return allOrganizations.organizations.map(org => ({ + ...org, + canSwitch: myOrgIds.has(org.id), + })); + }, [allOrganizations?.organizations, myOrganizations]); + // Use custom renderers or defaults with styles and translations const renderLoadingWithStyles = renderLoading || (() => defaultRenderLoading(t, styles)); const renderErrorWithStyles = renderError || ((error: string) => defaultRenderError(error, t, styles)); @@ -315,7 +316,7 @@ export const BaseOrganizationList: FC = ({ defaultRenderOrganization(org, styles, t, onOrganizationSelect, showStatus)); // Show loading state - if (isLoading && data.length === 0) { + if (isLoading && organizationsWithSwitchAccess?.length === 0) { const loadingContent = (
= ({ } // Show error state - if (error && data.length === 0) { + if (error && organizationsWithSwitchAccess?.length === 0) { const errorContent = (
= ({ } // Show empty state - if (!isLoading && data.length === 0) { + if (!isLoading && organizationsWithSwitchAccess?.length === 0) { const emptyContent = (
= ({ {/* Header with total count and refresh button */}
- {totalCount !== undefined && ( - - {t('organization.switcher.showing.count', {showing: data.length, total: totalCount})} - - )} + + {t('organization.switcher.showing.count', { + showing: organizationsWithSwitchAccess?.length, + total: allOrganizations?.organizations?.length || 0, + })} +
{onRefresh && ( *
- {isSelected && } + {isSelected && } ); @@ -564,7 +560,7 @@ export const BaseOrganizationSwitcher: FC = ({ style={{ ...styles.dropdownHeader, ...styles.sectionHeaderContainer, - borderTop: currentOrganization ? `1px solid ${theme.colors.border}` : 'none', + borderTop: currentOrganization ? `1px solid ${theme.vars.colors.border}` : 'none', }} > @@ -603,7 +599,7 @@ export const BaseOrganizationSwitcher: FC = ({ ...styles.menuItem, backgroundColor: hoveredItemIndex === switchableOrganizations.indexOf(organization) - ? hoverBackgroundColor + ? theme.vars.colors.secondary.main : 'transparent', }} onMouseEnter={(): void => setHoveredItemIndex(switchableOrganizations.indexOf(organization))} @@ -633,7 +629,7 @@ export const BaseOrganizationSwitcher: FC = ({ ...styles.menuItem, backgroundColor: hoveredItemIndex === switchableOrganizations.length + index - ? hoverBackgroundColor + ? theme.vars.colors.secondary.main : 'transparent', }} className={withVendorCSSClassPrefix('organization-switcher__menu-item')} @@ -652,7 +648,7 @@ export const BaseOrganizationSwitcher: FC = ({ ...styles.menuItem, backgroundColor: hoveredItemIndex === switchableOrganizations.length + index - ? hoverBackgroundColor + ? theme.vars.colors.secondary.main : 'transparent', }} className={withVendorCSSClassPrefix('organization-switcher__menu-item')} diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx index cc51086b2..95369ef14 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -36,6 +36,7 @@ import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; import {useForm, FormField} from '../../../hooks/useForm'; import useTranslation from '../../../hooks/useTranslation'; +import useTheme from '../../../contexts/Theme/useTheme'; import Alert from '../../primitives/Alert/Alert'; import Card, {CardProps} from '../../primitives/Card/Card'; import Divider from '../../primitives/Divider/Divider'; @@ -340,6 +341,7 @@ const BaseSignInContent: FC = ({ size = 'medium', variant = 'outlined', }: BaseSignInProps) => { + const {theme} = useTheme(); const {t} = useTranslation(); const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); @@ -424,7 +426,8 @@ const BaseSignInContent: FC = ({ */ const handleRedirectionIfNeeded = (response: EmbeddedSignInFlowHandleResponse): boolean => { if ( - response && 'nextStep' in response && + response && + 'nextStep' in response && response.nextStep && (response.nextStep as any).stepType === EmbeddedSignInFlowStepType.AuthenticatorPrompt && (response.nextStep as any).authenticators && @@ -1091,9 +1094,16 @@ const BaseSignInContent: FC = ({ return ( -
+
- + {t('messages.loading')}
@@ -1119,17 +1129,17 @@ const BaseSignInContent: FC = ({ {flowTitle || t('signin.title')} {flowSubtitle && ( - + {flowSubtitle || t('signin.subtitle')} )} {flowMessages && flowMessages.length > 0 && ( -
+
{flowMessages.map((flowMessage, index) => ( {flowMessage.message} @@ -1138,7 +1148,7 @@ const BaseSignInContent: FC = ({
)} {messages.length > 0 && ( -
+
{messages.map((message, index) => { const variant = message.type.toLowerCase() === 'error' @@ -1150,7 +1160,12 @@ const BaseSignInContent: FC = ({ : 'info'; return ( - + {message.message} ); @@ -1161,17 +1176,21 @@ const BaseSignInContent: FC = ({ {error && ( - + Error {error} )} -
+
{/* Render USER_PROMPT authenticators as form fields */} {userPromptAuthenticators.map((authenticator, index) => (
- {index > 0 && OR} + {index > 0 && OR}
{ e.preventDefault(); @@ -1201,7 +1220,7 @@ const BaseSignInContent: FC = ({ {/* Add divider between user prompts and option authenticators if both exist */} {userPromptAuthenticators.length > 0 && optionAuthenticators.length > 0 && ( - OR + OR )} {/* Render all other authenticators (REDIRECTION_PROMPT, multi-option buttons, etc.) */} @@ -1254,12 +1273,15 @@ const BaseSignInContent: FC = ({ return ( -
-
+
+
{t('passkey.authenticating') || 'Authenticating with passkey...'} - + {t('passkey.instruction') || 'Please use your fingerprint, face, or security key to authenticate.'}
@@ -1272,16 +1294,16 @@ const BaseSignInContent: FC = ({ {flowTitle || t('signin.title')} - + {flowSubtitle || t('signin.subtitle')} {flowMessages && flowMessages.length > 0 && ( -
+
{flowMessages.map((flowMessage, index) => ( {flowMessage.message} @@ -1290,7 +1312,7 @@ const BaseSignInContent: FC = ({
)} {messages.length > 0 && ( -
+
{messages.map((message, index) => { const variant = message.type.toLowerCase() === 'error' @@ -1302,7 +1324,12 @@ const BaseSignInContent: FC = ({ : 'info'; return ( - + {message.message} ); @@ -1313,7 +1340,11 @@ const BaseSignInContent: FC = ({ {error && ( - + {t('errors.title')} {error} diff --git a/packages/react/src/components/presentation/SignIn/options/EmailOtp.tsx b/packages/react/src/components/presentation/SignIn/options/EmailOtp.tsx index c542eaa85..e7451c78b 100644 --- a/packages/react/src/components/presentation/SignIn/options/EmailOtp.tsx +++ b/packages/react/src/components/presentation/SignIn/options/EmailOtp.tsx @@ -24,6 +24,7 @@ import OtpField from '../../../primitives/OtpField/OtpField'; import {BaseSignInOptionProps} from './SignInOptionFactory'; import useTranslation from '../../../../hooks/useTranslation'; import useFlow from '../../../../contexts/Flow/useFlow'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * Email OTP Sign-In Option Component. @@ -40,6 +41,7 @@ const EmailOtp: FC = ({ buttonClassName = '', preferences, }) => { + const {theme} = useTheme(); const {t} = useTranslation(preferences?.i18n); const {setTitle, setSubtitle} = useFlow(); @@ -61,7 +63,7 @@ const EmailOtp: FC = ({ const isOtpParam = param.param.toLowerCase().includes('otp') || param.param.toLowerCase().includes('code'); return ( -
+
{isOtpParam && hasOtpField ? ( = ({ color="primary" variant="solid" fullWidth - style={{marginBottom: '1rem'}} + style={{marginBottom: `calc(${theme.vars.spacing.unit} * 2)`}} > {t('email.otp.submit.button')} diff --git a/packages/react/src/components/presentation/SignIn/options/IdentifierFirst.tsx b/packages/react/src/components/presentation/SignIn/options/IdentifierFirst.tsx index aceadf88f..ab939a559 100644 --- a/packages/react/src/components/presentation/SignIn/options/IdentifierFirst.tsx +++ b/packages/react/src/components/presentation/SignIn/options/IdentifierFirst.tsx @@ -23,6 +23,7 @@ import Button from '../../../primitives/Button/Button'; import {BaseSignInOptionProps} from './SignInOptionFactory'; import useTranslation from '../../../../hooks/useTranslation'; import useFlow from '../../../../contexts/Flow/useFlow'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * Identifier First Sign-In Option Component. @@ -39,6 +40,7 @@ const IdentifierFirst: FC = ({ buttonClassName = '', preferences, }) => { + const {theme} = useTheme(); const {t} = useTranslation(preferences?.i18n); const {setTitle, setSubtitle} = useFlow(); @@ -52,7 +54,7 @@ const IdentifierFirst: FC = ({ return ( <> {formFields.map(param => ( -
+
{createField({ name: param.param, type: @@ -83,7 +85,7 @@ const IdentifierFirst: FC = ({ color="primary" variant="solid" fullWidth - style={{marginBottom: '1rem'}} + style={{marginBottom: `calc(${theme.vars.spacing.unit} * 2)`}} > {t('identifier.first.submit.button')} diff --git a/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx b/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx index 97776733c..dfdac78b5 100644 --- a/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx +++ b/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx @@ -94,7 +94,7 @@ const MultiOptionButton: FC = ({ ); case ApplicationNativeAuthenticationConstants.SupportedAuthenticators.Passkey: return ( - + diff --git a/packages/react/src/components/presentation/SignIn/options/SmsOtp.tsx b/packages/react/src/components/presentation/SignIn/options/SmsOtp.tsx index 9b325cf28..6d6c9257c 100644 --- a/packages/react/src/components/presentation/SignIn/options/SmsOtp.tsx +++ b/packages/react/src/components/presentation/SignIn/options/SmsOtp.tsx @@ -24,6 +24,7 @@ import OtpField from '../../../primitives/OtpField/OtpField'; import {BaseSignInOptionProps} from './SignInOptionFactory'; import useTranslation from '../../../../hooks/useTranslation'; import useFlow from '../../../../contexts/Flow/useFlow'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * SMS OTP Sign-In Option Component. @@ -40,6 +41,7 @@ const SmsOtp: FC = ({ buttonClassName = '', preferences, }) => { + const {theme} = useTheme(); const {t} = useTranslation(preferences?.i18n); const {setTitle, setSubtitle} = useFlow(); @@ -60,7 +62,7 @@ const SmsOtp: FC = ({ const isOtpParam = param.param.toLowerCase().includes('otp') || param.param.toLowerCase().includes('code'); return ( -
+
{isOtpParam && hasOtpField ? ( = ({ color="primary" variant="solid" fullWidth - style={{marginBottom: '1rem'}} + style={{marginBottom: `calc(${theme.vars.spacing.unit} * 2)`}} > {t('sms.otp.submit.button')} diff --git a/packages/react/src/components/presentation/SignIn/options/SocialButton.tsx b/packages/react/src/components/presentation/SignIn/options/SocialButton.tsx index bc660d29d..db4e80c9b 100644 --- a/packages/react/src/components/presentation/SignIn/options/SocialButton.tsx +++ b/packages/react/src/components/presentation/SignIn/options/SocialButton.tsx @@ -61,7 +61,7 @@ const SocialLogin: FC = ({ startIcon={ diff --git a/packages/react/src/components/presentation/SignIn/options/Totp.tsx b/packages/react/src/components/presentation/SignIn/options/Totp.tsx index b1d61328b..de1bcfcda 100644 --- a/packages/react/src/components/presentation/SignIn/options/Totp.tsx +++ b/packages/react/src/components/presentation/SignIn/options/Totp.tsx @@ -24,6 +24,7 @@ import OtpField from '../../../primitives/OtpField/OtpField'; import {BaseSignInOptionProps} from './SignInOptionFactory'; import useTranslation from '../../../../hooks/useTranslation'; import useFlow from '../../../../contexts/Flow/useFlow'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * TOTP Sign-In Option Component. @@ -40,6 +41,7 @@ const Totp: FC = ({ buttonClassName = '', preferences, }) => { + const {theme} = useTheme(); const {t} = useTranslation(preferences?.i18n); const {setTitle, setSubtitle} = useFlow(); @@ -60,7 +62,7 @@ const Totp: FC = ({ const isTotpParam = param.param.toLowerCase().includes('totp') || param.param.toLowerCase().includes('token'); return ( -
+
{isTotpParam && hasTotpField ? ( = ({ color="primary" variant="solid" fullWidth - style={{marginBottom: '1rem'}} + style={{marginBottom: `calc(${theme.vars.spacing.unit} * 2)`}} > {t('totp.submit.button')} diff --git a/packages/react/src/components/presentation/SignIn/options/UsernamePassword.tsx b/packages/react/src/components/presentation/SignIn/options/UsernamePassword.tsx index 602462d92..6278b4b84 100644 --- a/packages/react/src/components/presentation/SignIn/options/UsernamePassword.tsx +++ b/packages/react/src/components/presentation/SignIn/options/UsernamePassword.tsx @@ -23,6 +23,7 @@ import Button from '../../../primitives/Button/Button'; import {BaseSignInOptionProps} from './SignInOptionFactory'; import useTranslation from '../../../../hooks/useTranslation'; import useFlow from '../../../../contexts/Flow/useFlow'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * Username Password Sign-In Option Component. @@ -39,6 +40,7 @@ const UsernamePassword: FC = ({ buttonClassName = '', preferences, }) => { + const {theme} = useTheme(); const {t} = useTranslation(preferences?.i18n); const {setTitle, setSubtitle} = useFlow(); @@ -53,7 +55,7 @@ const UsernamePassword: FC = ({ return ( <> {formFields.map(param => ( -
+
{createField({ name: param.param, type: @@ -84,7 +86,7 @@ const UsernamePassword: FC = ({ color="primary" variant="solid" fullWidth - style={{marginBottom: '1rem'}} + style={{marginBottom: `calc(${theme.vars.spacing.unit} * 2)`}} > {t('username.password.submit.button')} diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index f2491f158..07525000b 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -32,6 +32,7 @@ import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; import {useForm, FormField} from '../../../hooks/useForm'; import useTranslation from '../../../hooks/useTranslation'; +import useTheme from '../../../contexts/Theme/useTheme'; import Alert from '../../primitives/Alert/Alert'; import Card, {CardProps} from '../../primitives/Card/Card'; import Spinner from '../../primitives/Spinner/Spinner'; @@ -180,6 +181,7 @@ const BaseSignUpContent: FC = ({ variant = 'outlined', isInitialized, }) => { + const {theme} = useTheme(); const {t} = useTranslation(); const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); @@ -634,7 +636,7 @@ const BaseSignUpContent: FC = ({ return ( -
+
@@ -659,12 +661,12 @@ const BaseSignUpContent: FC = ({ {flowMessages && flowMessages.length > 0 && ( -
+
{flowMessages.map((message: any, index: number) => ( {message.message} @@ -676,13 +678,17 @@ const BaseSignUpContent: FC = ({ {error && ( - + {t('errors.title') || 'Error'} {error} )} -
+
{currentFlow.data?.components && renderComponents(currentFlow.data.components)}
diff --git a/packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx b/packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx index 57c497116..a02a5acba 100644 --- a/packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx +++ b/packages/react/src/components/presentation/SignUp/options/DividerComponent.tsx @@ -19,11 +19,13 @@ import {FC} from 'react'; import {BaseSignUpOptionProps} from './SignUpOptionFactory'; import Divider from '../../../primitives/Divider/Divider'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * Divider component for sign-up forms. */ const DividerComponent: FC = ({component}) => { + const {theme} = useTheme(); const config = component.config || {}; const text = config['text'] || ''; const variant = component.variant?.toLowerCase() || 'horizontal'; @@ -32,7 +34,7 @@ const DividerComponent: FC = ({component}) => { {text} diff --git a/packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx b/packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx index 5ed54f86f..ce7537789 100644 --- a/packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx +++ b/packages/react/src/components/presentation/SignUp/options/ImageComponent.tsx @@ -18,11 +18,13 @@ import {FC} from 'react'; import {BaseSignUpOptionProps} from './SignUpOptionFactory'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * Image component for sign-up forms. */ const ImageComponent: FC = ({component}) => { + const {theme} = useTheme(); const config = component.config || {}; const src = config['src'] || ''; const alt = config['alt'] || config['label'] || 'Image'; @@ -33,7 +35,7 @@ const ImageComponent: FC = ({component}) => { height: 'auto', display: 'block', margin: variant === 'image_block' ? '1rem auto' : '0', - borderRadius: '4px', + borderRadius: theme.vars.borderRadius.small, }; if (!src) { diff --git a/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx b/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx index 80038530f..57f092400 100644 --- a/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx +++ b/packages/react/src/components/presentation/SignUp/options/SocialButton.tsx @@ -53,7 +53,7 @@ const SocialButton: FC = ({ startIcon={ diff --git a/packages/react/src/components/presentation/SignUp/options/Typography.tsx b/packages/react/src/components/presentation/SignUp/options/Typography.tsx index 566648d56..8bec84fd1 100644 --- a/packages/react/src/components/presentation/SignUp/options/Typography.tsx +++ b/packages/react/src/components/presentation/SignUp/options/Typography.tsx @@ -19,11 +19,13 @@ import {FC} from 'react'; import {BaseSignUpOptionProps} from './SignUpOptionFactory'; import Typography from '../../../primitives/Typography/Typography'; +import useTheme from '../../../../contexts/Theme/useTheme'; /** * Typography component for sign-up forms (titles, descriptions, etc.). */ const TypographyComponent: FC = ({component}) => { + const {theme} = useTheme(); const config = component.config || {}; const text = config['text'] || config['content'] || ''; const variant = component.variant?.toLowerCase() || 'body1'; @@ -67,7 +69,11 @@ const TypographyComponent: FC = ({component}) => { } return ( - + {text} ); diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 3ac927069..a3033831d 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -48,18 +48,18 @@ const useStyles = () => { trigger: { display: 'inline-flex', alignItems: 'center', - gap: `${theme.spacing.unit}px`, - padding: `${theme.spacing.unit * 0.5}px`, + gap: theme.vars.spacing.unit, + padding: `calc(${theme.vars.spacing.unit} * 0.5)`, border: 'none', background: 'none', cursor: 'pointer', - borderRadius: theme.borderRadius.medium, + borderRadius: theme.vars.borderRadius.medium, '&:hover': { - backgroundColor: theme.colors.background, + backgroundColor: theme.vars.colors.background.surface, }, } as CSSProperties, userName: { - color: theme.colors.text.primary, + color: theme.vars.colors.text.primary, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', @@ -69,12 +69,10 @@ const useStyles = () => { dropdownContent: { minWidth: '200px', maxWidth: '300px', - backgroundColor: theme.colors.background.surface, - borderRadius: theme.borderRadius.medium, - boxShadow: `0 4px 6px -1px ${ - colorScheme === 'dark' ? 'rgba(0, 0, 0, 0.4)' : 'rgba(0, 0, 0, 0.1)' - }, 0 2px 4px -1px ${colorScheme === 'dark' ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.06)'}`, - border: `1px solid ${theme.colors.border}`, + backgroundColor: theme.vars.colors.background.surface, + borderRadius: theme.vars.borderRadius.medium, + boxShadow: theme.vars.shadows.medium, + border: `1px solid ${theme.vars.colors.border}`, outline: 'none', zIndex: 1000, } as CSSProperties, @@ -87,51 +85,51 @@ const useStyles = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-start', - gap: `${theme.spacing.unit}px`, - padding: `${theme.spacing.unit * 1.5}px ${theme.spacing.unit * 2}px`, + gap: theme.vars.spacing.unit, + padding: `calc(${theme.vars.spacing.unit} * 1.5) calc(${theme.vars.spacing.unit} * 2)`, width: '100%', - color: theme.colors.text.primary, + color: theme.vars.colors.text.primary, textDecoration: 'none', border: 'none', background: 'none', cursor: 'pointer', fontSize: '0.875rem', textAlign: 'left', - borderRadius: theme.borderRadius.medium, + borderRadius: theme.vars.borderRadius.medium, transition: 'background-color 0.15s ease-in-out', } as CSSProperties, menuItemAnchor: { display: 'flex', alignItems: 'center', justifyContent: 'flex-start', - gap: `${theme.spacing.unit}px`, - padding: `${theme.spacing.unit * 1.5}px ${theme.spacing.unit * 2}px`, + gap: theme.vars.spacing.unit, + padding: `calc(${theme.vars.spacing.unit} * 1.5) calc(${theme.vars.spacing.unit} * 2)`, width: '100%', - color: theme.colors.text.primary, + color: theme.vars.colors.text.primary, textDecoration: 'none', border: 'none', background: 'none', cursor: 'pointer', fontSize: '0.875rem', textAlign: 'left', - borderRadius: theme.borderRadius.medium, + borderRadius: theme.vars.borderRadius.medium, transition: 'background-color 0.15s ease-in-out', } as CSSProperties, divider: { - margin: `${theme.spacing.unit * 0.5}px 0`, - borderBottom: `1px solid ${theme.colors.border}`, + margin: `calc(${theme.vars.spacing.unit} * 0.5) 0`, + borderBottom: `1px solid ${theme.vars.colors.border}`, } as CSSProperties, dropdownHeader: { display: 'flex', alignItems: 'center', - gap: `${theme.spacing.unit}px`, - padding: `${theme.spacing.unit * 1.5}px`, - borderBottom: `1px solid ${theme.colors.border}`, + gap: theme.vars.spacing.unit, + padding: `calc(${theme.vars.spacing.unit} * 1.5)`, + borderBottom: `1px solid ${theme.vars.colors.border}`, } as CSSProperties, headerInfo: { display: 'flex', flexDirection: 'column', - gap: `${theme.spacing.unit / 4}px`, + gap: `calc(${theme.vars.spacing.unit} / 4)`, flex: 1, minWidth: 0, overflow: 'hidden', @@ -139,7 +137,7 @@ const useStyles = () => { whiteSpace: 'nowrap', } as CSSProperties, headerName: { - color: theme.colors.text.primary, + color: theme.vars.colors.text.primary, fontSize: '1rem', fontWeight: 500, margin: 0, @@ -148,7 +146,7 @@ const useStyles = () => { whiteSpace: 'nowrap', } as CSSProperties, headerEmail: { - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, fontSize: '0.875rem', margin: 0, overflow: 'hidden', @@ -160,10 +158,10 @@ const useStyles = () => { alignItems: 'center', justifyContent: 'center', minHeight: '80px', - gap: `${theme.spacing.unit}px`, + gap: theme.vars.spacing.unit, } as CSSProperties, loadingText: { - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, fontSize: '0.875rem', } as CSSProperties, }), @@ -259,8 +257,6 @@ export const BaseUserDropdown: FC = ({ const [hoveredItemIndex, setHoveredItemIndex] = useState(null); const {theme, colorScheme} = useTheme(); - const hoverBackgroundColor = colorScheme === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'; - const {refs, floatingStyles, context} = useFloating({ open: isOpen, onOpenChange: setIsOpen, @@ -411,7 +407,8 @@ export const BaseUserDropdown: FC = ({ href={item.href} style={{ ...styles.menuItemAnchor, - backgroundColor: hoveredItemIndex === index ? hoverBackgroundColor : 'transparent', + backgroundColor: + hoveredItemIndex === index ? theme.vars.colors.secondary.main : 'transparent', }} className={withVendorCSSClassPrefix('user-dropdown__menu-item')} onMouseEnter={() => setHoveredItemIndex(index)} @@ -427,7 +424,8 @@ export const BaseUserDropdown: FC = ({ onClick={() => handleMenuItemClick(item)} style={{ ...styles.menuItem, - backgroundColor: hoveredItemIndex === index ? hoverBackgroundColor : 'transparent', + backgroundColor: + hoveredItemIndex === index ? theme.vars.colors.secondary.main : 'transparent', }} className={withVendorCSSClassPrefix('user-dropdown__menu-item')} color="tertiary" diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx index 231f70737..190853233 100644 --- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx @@ -174,11 +174,11 @@ const BaseUserProfile: FC = ({ {Object.entries(data).map(([key, value]) => ( - - + - @@ -276,9 +276,9 @@ const BaseUserProfile: FC = ({ const styles = useStyles(); const buttonStyle = useMemo( () => ({ - padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, - margin: `${theme.spacing.unit}px`, - borderRadius: theme.borderRadius.medium, + padding: `calc(${theme.vars.spacing.unit} * 1) calc(${theme.vars.spacing.unit} * 2)`, + margin: theme.vars.spacing.unit, + borderRadius: theme.vars.borderRadius.medium, border: 'none', cursor: 'pointer', fontSize: '0.875rem', @@ -290,8 +290,8 @@ const BaseUserProfile: FC = ({ const saveButtonStyle = useMemo( () => ({ ...buttonStyle, - backgroundColor: theme.colors.primary.main, - color: theme.colors.primary.contrastText, + backgroundColor: theme.vars.colors.primary.main, + color: theme.vars.colors.primary.contrastText, }), [theme, buttonStyle], ); @@ -299,8 +299,8 @@ const BaseUserProfile: FC = ({ const cancelButtonStyle = useMemo( () => ({ ...buttonStyle, - backgroundColor: theme.colors.secondary.main, - border: `1px solid ${theme.colors.border}`, + backgroundColor: theme.vars.colors.secondary.main, + border: `1px solid ${theme.vars.colors.border}`, }), [theme, buttonStyle], ); @@ -491,8 +491,8 @@ const BaseUserProfile: FC = ({ minHeight: '60px', width: '100%', padding: '8px', - border: '1px solid #ccc', - borderRadius: '4px', + border: `1px solid ${theme.vars.colors.border}`, + borderRadius: theme.vars.borderRadius.small, resize: 'vertical', }} /> @@ -570,12 +570,12 @@ const BaseUserProfile: FC = ({ ...styles.field, display: 'flex', alignItems: 'center', - gap: `${theme.spacing.unit}px`, + gap: theme.vars.spacing.unit, }; return (
-
+
{renderSchemaField( schema, isFieldEditing, @@ -591,9 +591,9 @@ const BaseUserProfile: FC = ({
{isFieldEditing && ( @@ -619,7 +619,7 @@ const BaseUserProfile: FC = ({ onClick={() => toggleFieldEdit(schema.name!)} title="Edit" style={{ - padding: `${theme.spacing.unit / 2}px`, + padding: `calc(${theme.vars.spacing.unit} / 2)`, }} > @@ -703,7 +703,7 @@ const BaseUserProfile: FC = ({ {title} -
{profileContent}
+
{profileContent}
); @@ -718,19 +718,19 @@ const useStyles = () => { return useMemo( () => ({ root: { - padding: `${theme.spacing.unit * 4}px`, + padding: `calc(${theme.vars.spacing.unit} * 4)`, minWidth: '600px', margin: '0 auto', } as CSSProperties, card: { - background: theme.colors.background.surface, - borderRadius: theme.borderRadius.large, + background: theme.vars.colors.background.surface, + borderRadius: theme.vars.borderRadius.large, } as CSSProperties, header: { display: 'flex', alignItems: 'center', - gap: `${theme.spacing.unit * 1.5}px`, - marginBottom: `${theme.spacing.unit * 1.5}px`, + gap: `calc(${theme.vars.spacing.unit} * 1.5)`, + marginBottom: `calc(${theme.vars.spacing.unit} * 1.5)`, } as CSSProperties, profileInfo: { flex: 1, @@ -739,18 +739,18 @@ const useStyles = () => { fontSize: '1.5rem', fontWeight: 600, margin: '0', - color: theme.colors.text.primary, + color: theme.vars.colors.text.primary, } as CSSProperties, infoContainer: { display: 'flex', flexDirection: 'column' as const, - gap: `${theme.spacing.unit}px`, + gap: theme.vars.spacing.unit, } as CSSProperties, field: { display: 'flex', alignItems: 'center', - padding: `${theme.spacing.unit}px 0`, - borderBottom: `1px solid ${theme.colors.border}`, + padding: `${theme.vars.spacing.unit} 0`, + borderBottom: `1px solid ${theme.vars.colors.border}`, minHeight: '32px', } as CSSProperties, lastField: { @@ -759,17 +759,17 @@ const useStyles = () => { label: { fontSize: '0.875rem', fontWeight: 500, - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, width: '120px', flexShrink: 0, lineHeight: '32px', } as CSSProperties, value: { - color: theme.colors.text.primary, + color: theme.vars.colors.text.primary, flex: 1, display: 'flex', alignItems: 'center', - gap: `${theme.spacing.unit}px`, + gap: theme.vars.spacing.unit, overflow: 'hidden', minHeight: '32px', '& input': { @@ -778,16 +778,16 @@ const useStyles = () => { }, lineHeight: '32px', '& table': { - backgroundColor: theme.colors.background, - borderRadius: theme.borderRadius.medium, + backgroundColor: theme.vars.colors.background.surface, + borderRadius: theme.vars.borderRadius.medium, whiteSpace: 'normal', }, '& td': { - borderColor: theme.colors.border, + borderColor: theme.vars.colors.border, }, } as CSSProperties, popup: { - padding: `${theme.spacing.unit * 2}px`, + padding: `calc(${theme.vars.spacing.unit} * 2)`, } as CSSProperties, }), [theme, colorScheme], diff --git a/packages/react/src/components/primitives/Alert/Alert.tsx b/packages/react/src/components/primitives/Alert/Alert.tsx index 9af01832a..8ff68571a 100644 --- a/packages/react/src/components/primitives/Alert/Alert.tsx +++ b/packages/react/src/components/primitives/Alert/Alert.tsx @@ -71,33 +71,33 @@ const useAlertStyles = (variant: AlertVariant) => { return useMemo(() => { const variantStyles: Record = { success: { - backgroundColor: '#d4edda', - borderColor: '#28a745', - color: '#155724', + backgroundColor: `${theme.vars.colors.success.main}15`, + borderColor: theme.vars.colors.success.main, + color: theme.vars.colors.success.main, }, error: { - backgroundColor: `${theme.colors.error.main}15`, - borderColor: theme.colors.error.main, - color: theme.colors.error.main, + backgroundColor: `${theme.vars.colors.error.main}15`, + borderColor: theme.vars.colors.error.main, + color: theme.vars.colors.error.main, }, warning: { - backgroundColor: '#fff3cd', - borderColor: '#ffc107', - color: '#856404', + backgroundColor: `${theme.vars.colors.warning.main}15`, + borderColor: theme.vars.colors.warning.main, + color: theme.vars.colors.warning.main, }, info: { - backgroundColor: `${theme.colors.primary.main}15`, - borderColor: theme.colors.primary.main, - color: theme.colors.primary.main, + backgroundColor: `${theme.vars.colors.primary.main}15`, + borderColor: theme.vars.colors.primary.main, + color: theme.vars.colors.primary.main, }, }; return { - padding: `${theme.spacing.unit * 2}px`, - borderRadius: theme.borderRadius.medium, + padding: `calc(${theme.vars.spacing.unit} * 2)`, + borderRadius: theme.vars.borderRadius.medium, border: '1px solid', display: 'flex', - gap: `${theme.spacing.unit * 1.5}px`, + gap: `calc(${theme.vars.spacing.unit} * 1.5)`, alignItems: 'flex-start', ...variantStyles[variant], }; @@ -110,9 +110,9 @@ const useAlertIconStyles = () => { return useMemo( (): CSSProperties => ({ flexShrink: 0, - marginTop: '2px', // Slight alignment adjustment - width: '20px', - height: '20px', + marginTop: `calc(${theme.vars.spacing.unit} * 0.25)`, // Slight alignment adjustment + width: `calc(${theme.vars.spacing.unit} * 2.5)`, + height: `calc(${theme.vars.spacing.unit} * 2.5)`, }), [theme], ); @@ -126,7 +126,7 @@ const useAlertContentStyles = () => { flex: 1, display: 'flex', flexDirection: 'column', - gap: `${theme.spacing.unit}px`, + gap: theme.vars.spacing.unit, }), [theme], ); @@ -138,7 +138,7 @@ const useAlertTitleStyles = () => { return useMemo( (): CSSProperties => ({ margin: 0, - fontSize: '14px', + fontSize: theme.vars.typography.fontSizes.sm, fontWeight: 600, lineHeight: 1.4, color: 'inherit', @@ -153,9 +153,9 @@ const useAlertDescriptionStyles = () => { return useMemo( (): CSSProperties => ({ margin: 0, - fontSize: '14px', + fontSize: theme.vars.typography.fontSizes.sm, lineHeight: 1.4, - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, }), [theme], ); diff --git a/packages/react/src/components/primitives/Avatar/Avatar.tsx b/packages/react/src/components/primitives/Avatar/Avatar.tsx index 6d4471285..c15d81621 100644 --- a/packages/react/src/components/primitives/Avatar/Avatar.tsx +++ b/packages/react/src/components/primitives/Avatar/Avatar.tsx @@ -75,10 +75,10 @@ const useStyles = ({ () => ({ avatar: { alignItems: 'center', - background: backgroundColor || theme.colors.background.surface, - border: backgroundColor ? 'none' : `1px solid ${theme.colors.border}`, + background: backgroundColor || theme.vars.colors.background.surface, + border: backgroundColor ? 'none' : `1px solid ${theme.vars.colors.border}`, borderRadius: variant === 'circular' ? '50%' : '8px', - color: backgroundColor ? '#ffffff' : theme.colors.text.primary, + color: backgroundColor ? '#ffffff' : theme.vars.colors.text.primary, display: 'flex', fontSize: `${size * 0.4}px`, fontWeight: 600, diff --git a/packages/react/src/components/primitives/Button/Button.tsx b/packages/react/src/components/primitives/Button/Button.tsx index d420f947c..5c5c88e4e 100644 --- a/packages/react/src/components/primitives/Button/Button.tsx +++ b/packages/react/src/components/primitives/Button/Button.tsx @@ -71,19 +71,19 @@ const useButtonStyles = ( // Size configurations const sizeConfig = { small: { - padding: `${theme.spacing.unit / 2}px ${theme.spacing.unit}px`, - fontSize: '0.75rem', - minHeight: '24px', + padding: `calc(${theme.vars.spacing.unit} * 0.5) calc(${theme.vars.spacing.unit} * 1)`, + fontSize: theme.vars.typography.fontSizes.sm, + minHeight: `calc(${theme.vars.spacing.unit} * 3)`, }, medium: { - padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, - fontSize: '0.875rem', - minHeight: '32px', + padding: `calc(${theme.vars.spacing.unit} * 1) calc(${theme.vars.spacing.unit} * 2)`, + fontSize: theme.vars.typography.fontSizes.md, + minHeight: `calc(${theme.vars.spacing.unit} * 4)`, }, large: { - padding: `${theme.spacing.unit * 1.5}px ${theme.spacing.unit * 3}px`, - fontSize: '1rem', - minHeight: '40px', + padding: `calc(${theme.vars.spacing.unit} * 1.5) calc(${theme.vars.spacing.unit} * 3)`, + fontSize: theme.vars.typography.fontSizes.lg, + minHeight: `calc(${theme.vars.spacing.unit} * 5)`, }, }; @@ -94,43 +94,43 @@ const useButtonStyles = ( switch (variant) { case 'solid': return { - backgroundColor: theme.colors.primary.main, - color: theme.colors.primary.contrastText, - border: `1px solid ${theme.colors.primary.main}`, + backgroundColor: theme.vars.colors.primary.main, + color: theme.vars.colors.primary.contrastText, + border: `1px solid ${theme.vars.colors.primary.main}`, '&:hover': { - backgroundColor: theme.colors.primary.main, + backgroundColor: theme.vars.colors.primary.main, opacity: 0.9, }, '&:active': { - backgroundColor: theme.colors.primary.main, + backgroundColor: theme.vars.colors.primary.main, opacity: 0.8, }, }; case 'outline': return { backgroundColor: 'transparent', - color: theme.colors.primary.main, - border: `1px solid ${theme.colors.primary.main}`, + color: theme.vars.colors.primary.main, + border: `1px solid ${theme.vars.colors.primary.main}`, '&:hover': { - backgroundColor: theme.colors.primary.main, - color: theme.colors.primary.contrastText, + backgroundColor: theme.vars.colors.primary.main, + color: theme.vars.colors.primary.contrastText, }, '&:active': { - backgroundColor: theme.colors.primary.main, - color: theme.colors.primary.contrastText, + backgroundColor: theme.vars.colors.primary.main, + color: theme.vars.colors.primary.contrastText, opacity: 0.9, }, }; case 'text': return { backgroundColor: 'transparent', - color: theme.colors.primary.main, + color: theme.vars.colors.primary.main, border: '1px solid transparent', '&:hover': { - backgroundColor: theme.colors.background.surface, + backgroundColor: theme.vars.colors.background.surface, }, '&:active': { - backgroundColor: theme.colors.background.surface, + backgroundColor: theme.vars.colors.background.surface, opacity: 0.8, }, }; @@ -140,43 +140,43 @@ const useButtonStyles = ( switch (variant) { case 'solid': return { - backgroundColor: theme.colors.secondary.main, - color: theme.colors.secondary.contrastText, - border: `1px solid ${theme.colors.secondary.main}`, + backgroundColor: theme.vars.colors.secondary.main, + color: theme.vars.colors.secondary.contrastText, + border: `1px solid ${theme.vars.colors.secondary.main}`, '&:hover': { - backgroundColor: theme.colors.secondary.main, + backgroundColor: theme.vars.colors.secondary.main, opacity: 0.9, }, '&:active': { - backgroundColor: theme.colors.secondary.main, + backgroundColor: theme.vars.colors.secondary.main, opacity: 0.8, }, }; case 'outline': return { backgroundColor: 'transparent', - color: theme.colors.secondary.main, - border: `1px solid ${theme.colors.secondary.main}`, + color: theme.vars.colors.secondary.main, + border: `1px solid ${theme.vars.colors.secondary.main}`, '&:hover': { - backgroundColor: theme.colors.secondary.main, - color: theme.colors.secondary.contrastText, + backgroundColor: theme.vars.colors.secondary.main, + color: theme.vars.colors.secondary.contrastText, }, '&:active': { - backgroundColor: theme.colors.secondary.main, - color: theme.colors.secondary.contrastText, + backgroundColor: theme.vars.colors.secondary.main, + color: theme.vars.colors.secondary.contrastText, opacity: 0.9, }, }; case 'text': return { backgroundColor: 'transparent', - color: theme.colors.secondary.main, + color: theme.vars.colors.secondary.main, border: '1px solid transparent', '&:hover': { - backgroundColor: theme.colors.background.surface, + backgroundColor: theme.vars.colors.background.surface, }, '&:active': { - backgroundColor: theme.colors.background.surface, + backgroundColor: theme.vars.colors.background.surface, opacity: 0.8, }, }; @@ -186,46 +186,46 @@ const useButtonStyles = ( switch (variant) { case 'solid': return { - backgroundColor: theme.colors.text.secondary, - color: theme.colors.background.surface, - border: `1px solid ${theme.colors.text.secondary}`, + backgroundColor: theme.vars.colors.text.secondary, + color: theme.vars.colors.background.surface, + border: `1px solid ${theme.vars.colors.text.secondary}`, '&:hover': { - backgroundColor: theme.colors.text.primary, - color: theme.colors.background.surface, + backgroundColor: theme.vars.colors.text.primary, + color: theme.vars.colors.background.surface, }, '&:active': { - backgroundColor: theme.colors.text.primary, - color: theme.colors.background.surface, + backgroundColor: theme.vars.colors.text.primary, + color: theme.vars.colors.background.surface, opacity: 0.9, }, }; case 'outline': return { backgroundColor: 'transparent', - color: theme.colors.text.secondary, - border: `1px solid ${theme.colors.border}`, + color: theme.vars.colors.text.secondary, + border: `1px solid ${theme.vars.colors.border}`, '&:hover': { - backgroundColor: theme.colors.background.surface, - borderColor: theme.colors.text.secondary, + backgroundColor: theme.vars.colors.background.surface, + borderColor: theme.vars.colors.text.secondary, }, '&:active': { - backgroundColor: theme.colors.background.surface, - borderColor: theme.colors.text.primary, + backgroundColor: theme.vars.colors.background.surface, + borderColor: theme.vars.colors.text.primary, opacity: 0.9, }, }; case 'text': return { backgroundColor: 'transparent', - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, border: '1px solid transparent', '&:hover': { - backgroundColor: theme.colors.background.surface, - color: theme.colors.text.primary, + backgroundColor: theme.vars.colors.background.surface, + color: theme.vars.colors.text.primary, }, '&:active': { - backgroundColor: theme.colors.background.surface, - color: theme.colors.text.primary, + backgroundColor: theme.vars.colors.background.surface, + color: theme.vars.colors.text.primary, opacity: 0.8, }, }; @@ -240,8 +240,8 @@ const useButtonStyles = ( display: 'inline-flex', alignItems: 'center', justifyContent: 'center', - gap: `${theme.spacing.unit}px`, - borderRadius: theme.borderRadius.medium, + gap: `calc(${theme.vars.spacing.unit} * 1)`, + borderRadius: theme.vars.borderRadius.medium, fontWeight: 500, cursor: disabled || loading ? 'not-allowed' : 'pointer', transition: 'all 0.2s ease-in-out', @@ -306,6 +306,7 @@ const Button = forwardRef( }, ref, ) => { + const {theme} = useTheme(); const buttonStyle = useButtonStyles(color, variant, size, fullWidth, disabled || false, loading); return ( @@ -331,8 +332,18 @@ const Button = forwardRef( size={size as SpinnerSize} color="currentColor" style={{ - width: size === 'small' ? '12px' : size === 'medium' ? '16px' : '20px', - height: size === 'small' ? '12px' : size === 'medium' ? '16px' : '20px', + width: + size === 'small' + ? `calc(${theme.vars.spacing.unit} * 1.5)` + : size === 'medium' + ? `calc(${theme.vars.spacing.unit} * 2)` + : `calc(${theme.vars.spacing.unit} * 2.5)`, + height: + size === 'small' + ? `calc(${theme.vars.spacing.unit} * 1.5)` + : size === 'medium' + ? `calc(${theme.vars.spacing.unit} * 2)` + : `calc(${theme.vars.spacing.unit} * 2.5)`, }} /> )} diff --git a/packages/react/src/components/primitives/Checkbox/Checkbox.tsx b/packages/react/src/components/primitives/Checkbox/Checkbox.tsx index ab01a1b10..519a4c738 100644 --- a/packages/react/src/components/primitives/Checkbox/Checkbox.tsx +++ b/packages/react/src/components/primitives/Checkbox/Checkbox.tsx @@ -56,10 +56,10 @@ const Checkbox: FC = ({label, error, className, required, helperT }; const inputStyle: CSSProperties = { - width: theme.spacing.unit * 2.5 + 'px', - height: theme.spacing.unit * 2.5 + 'px', - marginRight: theme.spacing.unit + 'px', - accentColor: theme.colors.primary.main, + width: `calc(${theme.vars.spacing.unit} * 2.5)`, + height: `calc(${theme.vars.spacing.unit} * 2.5)`, + marginRight: theme.vars.spacing.unit, + accentColor: theme.vars.colors.primary.main, }; return ( @@ -67,7 +67,7 @@ const Checkbox: FC = ({label, error, className, required, helperT error={error} helperText={helperText} className={clsx(withVendorCSSClassPrefix('checkbox'), className)} - helperTextMarginLeft={theme.spacing.unit * 3.5 + 'px'} + helperTextMarginLeft={`calc(${theme.vars.spacing.unit} * 3.5)`} >
@@ -77,7 +77,7 @@ const Checkbox: FC = ({label, error, className, required, helperT error={!!error} variant="inline" style={{ - color: error ? theme.colors.error.main : theme.colors.text.primary, + color: error ? theme.vars.colors.error.main : theme.vars.colors.text.primary, fontSize: '0.875rem', }} > diff --git a/packages/react/src/components/primitives/DatePicker/DatePicker.tsx b/packages/react/src/components/primitives/DatePicker/DatePicker.tsx index 2f138960f..2e8b7b993 100644 --- a/packages/react/src/components/primitives/DatePicker/DatePicker.tsx +++ b/packages/react/src/components/primitives/DatePicker/DatePicker.tsx @@ -69,12 +69,12 @@ const DatePicker: FC = ({ const inputStyle: CSSProperties = { width: '100%', - padding: `${theme.spacing.unit}px ${theme.spacing.unit * 1.5}px`, - border: `1px solid ${error ? theme.colors.error.main : theme.colors.border}`, - borderRadius: theme.borderRadius.medium, + padding: `${theme.vars.spacing.unit} calc(${theme.vars.spacing.unit} * 1.5)`, + border: `1px solid ${error ? theme.vars.colors.error.main : theme.vars.colors.border}`, + borderRadius: theme.vars.borderRadius.medium, fontSize: '1rem', - color: theme.colors.text.primary, - backgroundColor: disabled ? theme.colors.background.disabled : theme.colors.background.surface, + color: theme.vars.colors.text.primary, + backgroundColor: disabled ? theme.vars.colors.background.disabled : theme.vars.colors.background.surface, outline: 'none', transition: 'border-color 0.2s ease', }; diff --git a/packages/react/src/components/primitives/Divider/Divider.tsx b/packages/react/src/components/primitives/Divider/Divider.tsx index 25c2e5172..a95f1c63b 100644 --- a/packages/react/src/components/primitives/Divider/Divider.tsx +++ b/packages/react/src/components/primitives/Divider/Divider.tsx @@ -59,10 +59,10 @@ const useStyles = (orientation: DividerOrientation, variant: DividerVariant, col container: { display: 'inline-block', height: '100%', - minHeight: '1rem', + minHeight: `calc(${theme.vars.spacing.unit} * 2)`, width: '1px', borderLeft: `1px ${borderStyle} ${baseColor}`, - margin: `0 ${theme.spacing.unit}px`, + margin: `0 calc(${theme.vars.spacing.unit} * 1)`, }, }; } @@ -72,7 +72,7 @@ const useStyles = (orientation: DividerOrientation, variant: DividerVariant, col display: 'flex', alignItems: 'center', width: '100%', - margin: `${theme.spacing.unit * 2}px 0`, + margin: `calc(${theme.vars.spacing.unit} * 2) 0`, }; if (hasChildren) { @@ -84,8 +84,8 @@ const useStyles = (orientation: DividerOrientation, variant: DividerVariant, col borderTop: `1px ${borderStyle} ${baseColor}`, }, text: { - backgroundColor: theme.colors.background.surface, - padding: `0 ${theme.spacing.unit}px`, + backgroundColor: theme.vars.colors.background.surface, + padding: `0 calc(${theme.vars.spacing.unit} * 1)`, whiteSpace: 'nowrap' as const, }, }; diff --git a/packages/react/src/components/primitives/FormControl/FormControl.tsx b/packages/react/src/components/primitives/FormControl/FormControl.tsx index fa46b5a5f..918af365d 100644 --- a/packages/react/src/components/primitives/FormControl/FormControl.tsx +++ b/packages/react/src/components/primitives/FormControl/FormControl.tsx @@ -64,12 +64,12 @@ const FormControl: FC = ({ const {theme} = useTheme(); const containerStyle: CSSProperties = { - marginBottom: theme.spacing.unit * 2 + 'px', + marginBottom: `calc(${theme.vars.spacing.unit} * 2)`, ...style, }; const helperTextStyle: CSSProperties = { - marginTop: theme.spacing.unit / 2 + 'px', + marginTop: `calc(${theme.vars.spacing.unit} / 2)`, textAlign: helperTextAlign, ...(helperTextMarginLeft && {marginLeft: helperTextMarginLeft}), }; diff --git a/packages/react/src/components/primitives/InputLabel/InputLabel.tsx b/packages/react/src/components/primitives/InputLabel/InputLabel.tsx index 5b3382f30..78ce951bb 100644 --- a/packages/react/src/components/primitives/InputLabel/InputLabel.tsx +++ b/packages/react/src/components/primitives/InputLabel/InputLabel.tsx @@ -59,9 +59,9 @@ const InputLabel: FC = ({ const labelStyle: CSSProperties = { display: variant, - marginBottom: marginBottom || (variant === 'block' ? theme.spacing.unit + 'px' : '0'), - color: error ? theme.colors.error.main : theme.colors.text.secondary, - fontSize: '0.875rem', + marginBottom: marginBottom || (variant === 'block' ? `calc(${theme.vars.spacing.unit} + 1px)` : '0'), + color: error ? theme.vars.colors.error.main : theme.vars.colors.text.secondary, + fontSize: theme.vars.typography.fontSizes.sm, fontWeight: variant === 'block' ? 500 : 'normal', ...style, }; @@ -69,7 +69,7 @@ const InputLabel: FC = ({ return ( ); }; diff --git a/packages/react/src/components/primitives/KeyValueInput/KeyValueInput.tsx b/packages/react/src/components/primitives/KeyValueInput/KeyValueInput.tsx index c2a1aa90f..1d1653342 100644 --- a/packages/react/src/components/primitives/KeyValueInput/KeyValueInput.tsx +++ b/packages/react/src/components/primitives/KeyValueInput/KeyValueInput.tsx @@ -238,29 +238,29 @@ const KeyValueInput: FC = ({ container: { display: 'flex', flexDirection: 'column' as const, - gap: `${theme.spacing.unit / 2}px`, + gap: `calc(${theme.vars.spacing.unit} / 2)`, } as CSSProperties, label: { fontSize: '0.875rem', fontWeight: 500, - color: theme.colors.text.primary, - marginBottom: `${theme.spacing.unit / 2}px`, + color: theme.vars.colors.text.primary, + marginBottom: `calc(${theme.vars.spacing.unit} / 2)`, } as CSSProperties, pairsList: { display: 'flex', flexDirection: 'column' as const, - gap: `${theme.spacing.unit / 4}px`, + gap: `calc(${theme.vars.spacing.unit} / 4)`, } as CSSProperties, pairRow: { display: 'flex', alignItems: 'center', - gap: `${theme.spacing.unit / 2}px`, - padding: `${theme.spacing.unit / 2}px`, - borderRadius: theme.borderRadius.small, + gap: `calc(${theme.vars.spacing.unit} / 2)`, + padding: `calc(${theme.vars.spacing.unit} / 2)`, + borderRadius: theme.vars.borderRadius.small, backgroundColor: 'transparent', border: 'none', '&:hover': { - backgroundColor: theme.colors.background.surface, + backgroundColor: theme.vars.colors.background.surface, }, } as CSSProperties, pairInput: { @@ -270,12 +270,12 @@ const KeyValueInput: FC = ({ addRow: { display: 'flex', alignItems: 'center', - gap: `${theme.spacing.unit / 2}px`, - padding: `${theme.spacing.unit / 2}px`, + gap: `calc(${theme.vars.spacing.unit} / 2)`, + padding: `calc(${theme.vars.spacing.unit} / 2)`, border: 'none', - borderRadius: theme.borderRadius.small, + borderRadius: theme.vars.borderRadius.small, backgroundColor: 'transparent', - marginTop: `${theme.spacing.unit / 2}px`, + marginTop: `calc(${theme.vars.spacing.unit} / 2)`, } as CSSProperties, removeButton: { minWidth: 'auto', @@ -283,15 +283,15 @@ const KeyValueInput: FC = ({ height: '24px', padding: '0', backgroundColor: 'transparent', - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, border: 'none', - borderRadius: theme.borderRadius.small, + borderRadius: theme.vars.borderRadius.small, display: 'flex', alignItems: 'center', justifyContent: 'center', '&:hover': { - backgroundColor: theme.colors.background.surface, - color: theme.colors.error.main, + backgroundColor: theme.vars.colors.background.surface, + color: theme.vars.colors.error.main, }, } as CSSProperties, addButton: { @@ -300,46 +300,46 @@ const KeyValueInput: FC = ({ height: '24px', padding: '0', backgroundColor: 'transparent', - color: theme.colors.primary.main, + color: theme.vars.colors.primary.main, border: 'none', - borderRadius: theme.borderRadius.small, + borderRadius: theme.vars.borderRadius.small, display: 'flex', alignItems: 'center', justifyContent: 'center', '&:hover': { - backgroundColor: theme.colors.primary.main, - color: theme.colors.primary.contrastText, + backgroundColor: theme.vars.colors.primary.main, + color: theme.vars.colors.primary.contrastText, }, } as CSSProperties, helperText: { fontSize: '0.75rem', - color: error ? theme.colors.error.main : theme.colors.text.secondary, - marginTop: `${theme.spacing.unit / 2}px`, + color: error ? theme.vars.colors.error.main : theme.vars.colors.text.secondary, + marginTop: `calc(${theme.vars.spacing.unit} / 2)`, } as CSSProperties, emptyState: { - padding: `${theme.spacing.unit}px`, + padding: theme.vars.spacing.unit, textAlign: 'center' as const, - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, fontStyle: 'italic', fontSize: '0.75rem', } as CSSProperties, readOnlyPair: { display: 'flex', alignItems: 'center', - gap: `${theme.spacing.unit / 2}px`, - padding: `${theme.spacing.unit / 4}px 0`, + gap: `calc(${theme.vars.spacing.unit} / 2)`, + padding: `calc(${theme.vars.spacing.unit} / 4) 0`, minHeight: '20px', } as CSSProperties, readOnlyKey: { fontSize: '0.75rem', fontWeight: 500, - color: theme.colors.text.secondary, + color: theme.vars.colors.text.secondary, minWidth: '80px', flexShrink: 0, } as CSSProperties, readOnlyValue: { fontSize: '0.75rem', - color: theme.colors.text.primary, + color: theme.vars.colors.text.primary, wordBreak: 'break-word' as const, flex: 1, } as CSSProperties, @@ -350,7 +350,7 @@ const KeyValueInput: FC = ({ {label && ( )} diff --git a/packages/react/src/components/primitives/OtpField/OtpField.tsx b/packages/react/src/components/primitives/OtpField/OtpField.tsx index c013ec3d3..970d75db8 100644 --- a/packages/react/src/components/primitives/OtpField/OtpField.tsx +++ b/packages/react/src/components/primitives/OtpField/OtpField.tsx @@ -134,29 +134,29 @@ const OtpField: FC = ({ const inputContainerStyle: CSSProperties = { display: 'flex', - gap: theme.spacing.unit + 'px', + gap: theme.vars.spacing.unit, justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', }; const inputStyle: CSSProperties = { - width: theme.spacing.unit * 6 + 'px', - height: theme.spacing.unit * 6 + 'px', + width: `calc(${theme.vars.spacing.unit} * 6)`, + height: `calc(${theme.vars.spacing.unit} * 6)`, textAlign: 'center', - fontSize: '1.25rem', + fontSize: theme.vars.typography.fontSizes.xl, fontWeight: 500, - border: `2px solid ${error ? theme.colors.error.main : theme.colors.border}`, - borderRadius: theme.borderRadius.medium, - color: theme.colors.text.primary, - backgroundColor: disabled ? theme.colors.background.disabled : theme.colors.background.surface, + border: `2px solid ${error ? theme.vars.colors.error.main : theme.vars.colors.border}`, + borderRadius: theme.vars.borderRadius.medium, + color: theme.vars.colors.text.primary, + backgroundColor: disabled ? theme.vars.colors.background.disabled : theme.vars.colors.background.surface, outline: 'none', transition: 'border-color 0.2s ease, box-shadow 0.2s ease', }; const focusedInputStyle: CSSProperties = { - borderColor: error ? theme.colors.error.main : theme.colors.primary.main, - boxShadow: `0 0 0 2px ${error ? theme.colors.error.main + '20' : theme.colors.primary.main + '20'}`, + borderColor: error ? theme.vars.colors.error.main : theme.vars.colors.primary.main, + boxShadow: `0 0 0 2px ${error ? theme.vars.colors.error.main + '20' : theme.vars.colors.primary.main + '20'}`, }; const handleChange = (index: number, event: ChangeEvent) => { @@ -276,13 +276,13 @@ const OtpField: FC = ({ onKeyDown={event => handleKeyDown(index, event)} onPaste={handlePaste} onFocus={event => { - event.target.style.borderColor = error ? theme.colors.error.main : theme.colors.primary.main; + event.target.style.borderColor = error ? theme.vars.colors.error.main : theme.vars.colors.primary.main; event.target.style.boxShadow = `0 0 0 2px ${ - error ? theme.colors.error.main + '20' : theme.colors.primary.main + '20' + error ? theme.vars.colors.error.main + '20' : theme.vars.colors.primary.main + '20' }`; }} onBlur={event => { - event.target.style.borderColor = error ? theme.colors.error.main : theme.colors.border; + event.target.style.borderColor = error ? theme.vars.colors.error.main : theme.vars.colors.border; event.target.style.boxShadow = 'none'; }} style={inputStyle} diff --git a/packages/react/src/components/primitives/Select/Select.tsx b/packages/react/src/components/primitives/Select/Select.tsx index 66b5c84a5..60dcd4748 100644 --- a/packages/react/src/components/primitives/Select/Select.tsx +++ b/packages/react/src/components/primitives/Select/Select.tsx @@ -80,17 +80,19 @@ const Select: FC = ({ const selectStyle: CSSProperties = { width: '100%', - padding: `${theme.spacing.unit}px ${theme.spacing.unit * 1.5}px`, - border: `1px solid ${error ? theme.colors.error.main : theme.colors.border}`, - borderRadius: theme.borderRadius.medium, - fontSize: '1rem', - color: theme.colors.text.primary, - backgroundColor: disabled ? theme.colors.background.disabled : theme.colors.background.surface, + padding: `${theme.vars.spacing.unit} calc(${theme.vars.spacing.unit} * 1.5)`, + border: `1px solid ${error ? theme.vars.colors.error.main : theme.vars.colors.border}`, + borderRadius: theme.vars.borderRadius.medium, + fontSize: theme.vars.typography.fontSizes.md, + color: theme.vars.colors.text.primary, + backgroundColor: disabled ? theme.vars.colors.background.disabled : theme.vars.colors.background.surface, outline: 'none', transition: 'border-color 0.2s ease', appearance: 'none', - backgroundImage: - "url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23${theme.colors.text.secondary.replace('#', '')}%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E')", + backgroundImage: `url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23${theme.colors.text.secondary.replace( + '#', + '', + )}%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E')`, backgroundRepeat: 'no-repeat', backgroundPosition: 'right .7em top 50%', backgroundSize: '.65em auto', diff --git a/packages/react/src/components/primitives/Spinner/Spinner.tsx b/packages/react/src/components/primitives/Spinner/Spinner.tsx index d56ac803d..01f926159 100644 --- a/packages/react/src/components/primitives/Spinner/Spinner.tsx +++ b/packages/react/src/components/primitives/Spinner/Spinner.tsx @@ -66,7 +66,7 @@ const Spinner: FC = ({size = 'medium', color, className, style}) = large: '32px', }[size]; - const spinnerColor = color || theme.colors.primary.main; + const spinnerColor = color || theme.vars.colors.primary.main; const spinnerStyle: CSSProperties = { width: spinnerSize, diff --git a/packages/react/src/components/primitives/Typography/Typography.tsx b/packages/react/src/components/primitives/Typography/Typography.tsx index f386e0383..473d2f2b9 100644 --- a/packages/react/src/components/primitives/Typography/Typography.tsx +++ b/packages/react/src/components/primitives/Typography/Typography.tsx @@ -173,84 +173,84 @@ const Typography: FC = ({ switch (variantName) { case 'h1': return { - fontSize: '2.125rem', // 34px + fontSize: theme.vars.typography.fontSizes['3xl'], // 34px fontWeight: 600, lineHeight: 1.235, letterSpacing: '-0.00735em', }; case 'h2': return { - fontSize: '1.5rem', // 24px + fontSize: theme.vars.typography.fontSizes['2xl'], // 24px fontWeight: 600, lineHeight: 1.334, letterSpacing: '0em', }; case 'h3': return { - fontSize: '1.25rem', // 20px + fontSize: theme.vars.typography.fontSizes.xl, // 20px fontWeight: 600, lineHeight: 1.6, letterSpacing: '0.0075em', }; case 'h4': return { - fontSize: '1.125rem', // 18px + fontSize: theme.vars.typography.fontSizes.lg, // 18px fontWeight: 600, lineHeight: 1.5, letterSpacing: '0.00938em', }; case 'h5': return { - fontSize: '1rem', // 16px + fontSize: theme.vars.typography.fontSizes.md, // 16px fontWeight: 600, lineHeight: 1.334, letterSpacing: '0em', }; case 'h6': return { - fontSize: '0.875rem', // 14px + fontSize: theme.vars.typography.fontSizes.sm, // 14px fontWeight: 500, lineHeight: 1.6, letterSpacing: '0.0075em', }; case 'subtitle1': return { - fontSize: '1rem', // 16px + fontSize: theme.vars.typography.fontSizes.md, // 16px fontWeight: 400, lineHeight: 1.75, letterSpacing: '0.00938em', }; case 'subtitle2': return { - fontSize: '0.875rem', // 14px + fontSize: theme.vars.typography.fontSizes.sm, // 14px fontWeight: 500, lineHeight: 1.57, letterSpacing: '0.00714em', }; case 'body1': return { - fontSize: '1rem', // 16px + fontSize: theme.vars.typography.fontSizes.md, // 16px fontWeight: 400, lineHeight: 1.5, letterSpacing: '0.00938em', }; case 'body2': return { - fontSize: '0.875rem', // 14px + fontSize: theme.vars.typography.fontSizes.sm, // 14px fontWeight: 400, lineHeight: 1.43, letterSpacing: '0.01071em', }; case 'caption': return { - fontSize: '0.75rem', // 12px + fontSize: theme.vars.typography.fontSizes.xs, // 12px fontWeight: 400, lineHeight: 1.66, letterSpacing: '0.03333em', }; case 'overline': return { - fontSize: '0.75rem', // 12px + fontSize: theme.vars.typography.fontSizes.xs, // 12px fontWeight: 400, lineHeight: 2.66, letterSpacing: '0.08333em', @@ -258,7 +258,7 @@ const Typography: FC = ({ }; case 'button': return { - fontSize: '0.875rem', // 14px + fontSize: theme.vars.typography.fontSizes.sm, // 14px fontWeight: 500, lineHeight: 1.75, letterSpacing: '0.02857em', From c56bd2af1df477211036c3c24bbb0913ebee531c Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 12:16:36 +0530 Subject: [PATCH 11/31] chore: remove outdated COMPLETE GUIDE.md file --- packages/react/COMPLETE GUIDE.md | 1123 ------------------------------ 1 file changed, 1123 deletions(-) delete mode 100644 packages/react/COMPLETE GUIDE.md diff --git a/packages/react/COMPLETE GUIDE.md b/packages/react/COMPLETE GUIDE.md deleted file mode 100644 index 7d5c3cafb..000000000 --- a/packages/react/COMPLETE GUIDE.md +++ /dev/null @@ -1,1123 +0,0 @@ -# @asgardeo/react - Complete Guide - -A comprehensive guide to building React applications with Asgardeo authentication using the official Asgardeo React SDK. - -## Table of Contents - -- [Overview](#overview) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [Configuration](#configuration) -- [Core Concepts](#core-concepts) -- [Components](#components) -- [Hooks](#hooks) -- [Advanced Usage](#advanced-usage) -- [Examples](#examples) -- [Troubleshooting](#troubleshooting) -- [API Reference](#api-reference) - -## Overview - -The `@asgardeo/react` SDK enables seamless authentication integration in React applications using Asgardeo Identity Server. It provides: - -- **Drop-in Components**: Pre-built UI components for authentication flows -- **React Hooks**: Programmatic access to authentication state and methods -- **Context Providers**: Global state management for authentication -- **Customizable UI**: Theming and styling options -- **TypeScript Support**: Full type safety and IntelliSense -- **Modern React**: Support for React 16.8+ with hooks - -## Installation - -```bash -# Using npm -npm install @asgardeo/react - -# Using pnpm -pnpm add @asgardeo/react - -# Using yarn -yarn add @asgardeo/react -``` - -### Peer Dependencies - -The SDK requires the following peer dependencies: - -```json -{ - "@types/react": ">=16.8.0", - "react": ">=16.8.0" -} -``` - -## Quick Start - -### 1. Set Up the Provider - -Wrap your application with `AsgardeoProvider` in your main entry file: - -#### Basic Setup - -```tsx -// main.tsx or index.tsx -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { AsgardeoProvider } from '@asgardeo/react' -import App from './App' - -createRoot(document.getElementById('root')!).render( - - - - - -) -``` - -#### Advanced Setup - -```tsx -// main.tsx or index.tsx -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { AsgardeoProvider } from '@asgardeo/react' -import App from './App' - -createRoot(document.getElementById('root')!).render( - - - - - -) -``` - -### 2. Build Your App - -Use the authentication components and hooks in your application: - -```tsx -// App.tsx -import { SignedIn, SignedOut, SignInButton, SignOutButton, User } from '@asgardeo/react' - -function App() { - return ( -
- -
- - {({ user }) => ( -
-

Welcome, {user.givenname}!

-

{user.email}

-
- )} -
- -
-
- - -
-

Welcome to Our App

-

Please sign in to continue

- -
-
-
- ) -} - -export default App -``` - -## Configuration - -### AsgardeoProvider Props - -| Prop | Type | Required | Description | -|------|------|----------|-------------| -| `baseUrl` | `string` | ✅ | Your Asgardeo organization URL | -| `clientId` | `string` | ✅ | Your application's client ID | -| `afterSignInUrl` | `string` | ❌ | Redirect URL after successful sign-in | -| `afterSignOutUrl` | `string` | ❌ | Redirect URL after sign-out | -| `scopes` | `string[]` | ❌ | OAuth 2.0 scopes (default: `['openid', 'profile']`) | -| `preferences` | `object` | ❌ | Theme and UI customization options | - -### Environment Variables - -For better security and flexibility, use environment variables: - -```tsx -// .env -VITE_ASGARDEO_BASE_URL=https://api.asgardeo.io/t/your-org -VITE_ASGARDEO_CLIENT_ID=your-client-id -VITE_ASGARDEO_AFTER_SIGN_IN_URL=http://localhost:3000/dashboard -VITE_ASGARDEO_AFTER_SIGN_OUT_URL=http://localhost:3000 - -// main.tsx - - - -``` - -## Core Concepts - -### Authentication State - -The SDK manages authentication state globally through React Context: - -- **Loading State**: While determining authentication status -- **Signed In**: User is authenticated with valid tokens -- **Signed Out**: User is not authenticated -- **Error State**: Authentication errors or failures - -### Token Management - -Automatic handling of: -- Access tokens for API calls -- ID tokens for user information -- Refresh tokens for session management -- Token renewal and expiration - -### User Context - -Access to user information including: -- Profile data (name, email, etc.) -- Claims and attributes -- Authentication metadata - -## Components - -### Control Components - -#### SignedIn - -Renders children only when the user is authenticated: - -```tsx -import { SignedIn } from '@asgardeo/react' - - -
This content is only visible to authenticated users
-
-``` - -#### SignedOut - -Renders children only when the user is not authenticated: - -```tsx -import { SignedOut } from '@asgardeo/react' - - -
Please sign in to access this application
-
-``` - -#### Loading - -Shows content while authentication state is being determined: - -```tsx -import { Loading } from '@asgardeo/react' - - -
Checking authentication status...
-
-``` - -#### Loaded - -Shows content after authentication state has been determined: - -```tsx -import { Loaded } from '@asgardeo/react' - - -
Authentication check complete
-
-``` - -### Action Components - -#### SignInButton - -Pre-built sign-in button: - -```tsx -import { SignInButton } from '@asgardeo/react' - -// Basic usage - - -// With custom styling - - -// With custom text - - Log In to Your Account - -``` - -#### SignOutButton - -Pre-built sign-out button: - -```tsx -import { SignOutButton } from '@asgardeo/react' - -// Basic usage - - -// With custom styling - - -// With custom text - - Log Out - -``` - -#### SignUpButton - -Pre-built sign-up button: - -```tsx -import { SignUpButton } from '@asgardeo/react' - - -``` - -### Presentation Components - -#### User - -Access user information with render props: - -```tsx -import { User } from '@asgardeo/react' - - - {({ user, isLoading, error }) => { - if (isLoading) return
Loading user...
- if (error) return
Error: {error.message}
- - return ( -
- {user.username} -

{user.givenname} {user.familyname}

-

{user.email}

-
- ) - }} -
-``` - -#### UserProfile - -Complete user profile component: - -```tsx -import { UserProfile } from '@asgardeo/react' - -// Basic usage - - -// With custom styling - -``` - -#### UserDropdown - -User menu dropdown component: - -```tsx -import { UserDropdown } from '@asgardeo/react' - - -``` - -#### SignIn - -Complete sign-in form component: - -```tsx -import { SignIn } from '@asgardeo/react' - - { - console.log('Sign-in successful:', authData) - // Handle successful sign-in - }} - onError={(error) => { - console.error('Sign-in failed:', error) - // Handle sign-in error - }} -/> -``` - -#### SignUp - -Complete sign-up form component: - -```tsx -import { SignUp } from '@asgardeo/react' - - { - console.log('Sign-up successful:', authData) - }} - onError={(error) => { - console.error('Sign-up failed:', error) - }} -/> -``` - -### Primitive Components - -The SDK includes low-level UI primitives for building custom interfaces: - -- `Button` - Customizable button component -- `TextField` - Text input field -- `PasswordField` - Password input with visibility toggle -- `Card` - Container component -- `Alert` - Message display component -- `Spinner` - Loading indicator -- `Typography` - Text styling component - -```tsx -import { Button, TextField, Card } from '@asgardeo/react' - - - - - -``` - -## Hooks - -### useAsgardeo - -Main hook for accessing authentication state and methods: - -```tsx -import { useAsgardeo } from '@asgardeo/react' - -function MyComponent() { - const { - user, - isSignedIn, - isLoading, - error, - signIn, - signOut, - getAccessToken, - getIdToken, - refreshTokens - } = useAsgardeo() - - const handleProtectedApiCall = async () => { - try { - const token = await getAccessToken() - const response = await fetch('/api/protected', { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }) - const data = await response.json() - console.log(data) - } catch (error) { - console.error('API call failed:', error) - } - } - - if (isLoading) { - return
Loading...
- } - - return ( -
- {isSignedIn ? ( -
-

Welcome, {user?.givenname}!

- - -
- ) : ( - - )} -
- ) -} -``` - -### useUser - -Access user-specific data and operations: - -```tsx -import { useUser } from '@asgardeo/react' - -function UserComponent() { - const { user, isLoading, error, refreshUser } = useUser() - - if (isLoading) return
Loading user data...
- if (error) return
Error: {error.message}
- - return ( -
-

{user.givenname} {user.familyname}

-

Email: {user.email}

-

Username: {user.username}

- -
- ) -} -``` - -### useTheme - -Access and customize theme settings: - -```tsx -import { useTheme } from '@asgardeo/react' - -function ThemedComponent() { - const { theme, setTheme } = useTheme() - - return ( -
-

Current theme mode: {theme.mode}

- -
- ) -} -``` - -### useI18n - -Internationalization support: - -```tsx -import { useI18n } from '@asgardeo/react' - -function LocalizedComponent() { - const { t, language, setLanguage } = useI18n() - - return ( -
-

{t('welcome.title')}

-

{t('welcome.description')}

- -
- ) -} -``` - -## Advanced Usage - -### Custom Authentication Flow - -For advanced use cases, you can implement custom authentication flows: - -```tsx -import { BaseSignIn } from '@asgardeo/react' - -function CustomSignIn() { - const handleInitialize = async () => { - // Custom initialization logic - return await initializeCustomAuth() - } - - const handleSubmit = async (payload) => { - // Custom authentication handling - return await handleCustomAuth(payload) - } - - const handleSuccess = (authData) => { - // Custom success handling - console.log('Authentication successful:', authData) - // Redirect or update UI - } - - const handleError = (error) => { - // Custom error handling - console.error('Authentication failed:', error) - // Show error message - } - - return ( - - ) -} -``` - -### Protected Routes - -Implement route protection with React Router: - -```tsx -import { Navigate, useLocation } from 'react-router-dom' -import { useAsgardeo } from '@asgardeo/react' - -function ProtectedRoute({ children }) { - const { isSignedIn, isLoading } = useAsgardeo() - const location = useLocation() - - if (isLoading) { - return
Loading...
- } - - if (!isSignedIn) { - return - } - - return children -} - -// Usage - - } /> - - - - } /> - -``` - -### API Integration - -Integrate with protected APIs: - -```tsx -import { useAsgardeo } from '@asgardeo/react' - -function useApi() { - const { getAccessToken } = useAsgardeo() - - const apiCall = async (url, options = {}) => { - const token = await getAccessToken() - - const response = await fetch(url, { - ...options, - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - ...options.headers - } - }) - - if (!response.ok) { - throw new Error(`API call failed: ${response.statusText}`) - } - - return response.json() - } - - return { apiCall } -} - -// Usage in component -function DataComponent() { - const { apiCall } = useApi() - const [data, setData] = useState(null) - const [loading, setLoading] = useState(false) - - const fetchData = async () => { - setLoading(true) - try { - const result = await apiCall('/api/user-data') - setData(result) - } catch (error) { - console.error('Failed to fetch data:', error) - } finally { - setLoading(false) - } - } - - return ( -
- - {data &&
{JSON.stringify(data, null, 2)}
} -
- ) -} -``` - -### Theme Customization - -Customize the appearance of components: - -```tsx -const customTheme = { - mode: 'light', - overrides: { - colors: { - primary: { - main: '#1976d2', - contrastText: '#ffffff' - }, - secondary: { - main: '#dc004e', - contrastText: '#ffffff' - } - }, - typography: { - fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif' - }, - spacing: { - unit: 8 - } - } -} - - - - -``` - -### Error Handling - -Implement comprehensive error handling: - -```tsx -import { useAsgardeo } from '@asgardeo/react' - -function ErrorBoundary({ children }) { - const { error, isLoading } = useAsgardeo() - - if (error) { - return ( -
-

Authentication Error

-

{error.message}

- -
- ) - } - - return children -} - -function App() { - return ( - -
- {/* Your app content */} -
-
- ) -} -``` - -## Examples - -### Complete Dashboard App - -```tsx -import { - AsgardeoProvider, - SignedIn, - SignedOut, - useAsgardeo, - User, - SignInButton, - SignOutButton -} from '@asgardeo/react' - -// Main App Component -function App() { - return ( - - - - ) -} - -// Layout Component -function Layout() { - return ( -
-
-
- - - - - - -
-
- ) -} - -// Header Component -function Header() { - return ( -
-
-

My App

- - -
- - {({ user }) => ( - Welcome, {user.givenname}! - )} - - -
-
- - - - -
-
- ) -} - -// Dashboard Component -function Dashboard() { - const { user, getAccessToken } = useAsgardeo() - const [data, setData] = useState(null) - const [loading, setLoading] = useState(false) - - const fetchUserData = async () => { - setLoading(true) - try { - const token = await getAccessToken() - const response = await fetch('/api/user/profile', { - headers: { - 'Authorization': `Bearer ${token}` - } - }) - const userData = await response.json() - setData(userData) - } catch (error) { - console.error('Failed to fetch user data:', error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - fetchUserData() - }, []) - - return ( -
-
-

Dashboard

-
-
-

Profile Information

-

Name: {user?.givenname} {user?.familyname}

-

Email: {user?.email}

-

Username: {user?.username}

-
- -
-

Additional Data

- {loading ? ( -

Loading...

- ) : data ? ( -
-                {JSON.stringify(data, null, 2)}
-              
- ) : ( -

No additional data available

- )} -
-
-
-
- ) -} - -// Landing Page Component -function LandingPage() { - return ( -
-
-

- Welcome to My App -

-

- A secure application powered by Asgardeo -

-
- -
- - Get Started - Sign In - -

- Don't have an account? Contact your administrator. -

-
-
- ) -} - -export default App -``` - -### Multi-Step Authentication Flow - -```tsx -import { BaseSignIn, useFlow } from '@asgardeo/react' - -function MultiStepAuth() { - const { currentStep, messages } = useFlow() - - const handleInitialize = async () => { - return await initializeAuthFlow() - } - - const handleSubmit = async (payload) => { - return await processAuthStep(payload) - } - - const handleSuccess = (authData) => { - // Redirect to dashboard - window.location.href = '/dashboard' - } - - const handleError = (error) => { - console.error('Authentication error:', error) - } - - return ( -
-
-

Sign In

- - {messages.map((message, index) => ( -
- {message.message} -
- ))} - - - -
-

Step {currentStep?.stepType || 1} of the authentication process

-
-
-
- ) -} -``` - -## Troubleshooting - -### Common Issues - -#### 1. "useAsgardeo must be used within AsgardeoProvider" - -**Problem**: Hook is used outside of provider context. - -**Solution**: Ensure your component is wrapped with `AsgardeoProvider`: - -```tsx -// ❌ Wrong -function App() { - const { isSignedIn } = useAsgardeo() // Error! - return
App
-} - -// ✅ Correct -function App() { - return ( - - - - ) -} - -function MyComponent() { - const { isSignedIn } = useAsgardeo() // Works! - return
Component
-} -``` - -#### 2. Infinite loading state - -**Problem**: Authentication state never resolves. - -**Solution**: Check configuration and network connectivity: - -```tsx -// Add error handling -const { isLoading, error } = useAsgardeo() - -if (error) { - console.error('Auth error:', error) - // Handle error appropriately -} -``` - -#### 3. CORS errors - -**Problem**: Cross-origin requests blocked. - -**Solution**: Configure CORS in your Asgardeo application settings or use a proxy during development. - -#### 4. Token expiration - -**Problem**: API calls fail due to expired tokens. - -**Solution**: Implement automatic token refresh: - -```tsx -const { refreshTokens, getAccessToken } = useAsgardeo() - -const apiCall = async (url, options) => { - try { - const token = await getAccessToken() - return await fetch(url, { - ...options, - headers: { - 'Authorization': `Bearer ${token}`, - ...options.headers - } - }) - } catch (error) { - if (error.status === 401) { - await refreshTokens() - const newToken = await getAccessToken() - return await fetch(url, { - ...options, - headers: { - 'Authorization': `Bearer ${newToken}`, - ...options.headers - } - }) - } - throw error - } -} -``` - -### Debugging Tips - -1. **Enable Debug Logs**: Set up console logging for authentication events -2. **Check Network Tab**: Verify API calls and responses -3. **Validate Configuration**: Ensure all required props are provided -4. **Test in Incognito**: Rule out cache/storage issues -5. **Check Asgardeo Console**: Verify application configuration - -## API Reference - -For complete API documentation including all components, hooks, and customization options, see [API.md](./API.md). - -### Key Exports - -```typescript -// Providers -export { AsgardeoProvider } from '@asgardeo/react' - -// Hooks -export { useAsgardeo, useUser, useTheme, useI18n } from '@asgardeo/react' - -// Control Components -export { SignedIn, SignedOut, Loading, Loaded } from '@asgardeo/react' - -// Action Components -export { SignInButton, SignOutButton, SignUpButton } from '@asgardeo/react' - -// Presentation Components -export { SignIn, SignUp, User, UserProfile, UserDropdown } from '@asgardeo/react' - -// Primitive Components -export { Button, TextField, Card, Alert, Spinner } from '@asgardeo/react' -``` - -### TypeScript Support - -The SDK is written in TypeScript and provides full type definitions: - -```typescript -import type { - AsgardeoProviderProps, - User, - AuthState, - SignInOptions, - ThemeConfig -} from '@asgardeo/react' -``` - ---- - -## Support - -- **Documentation**: [Complete API Reference](./API.md) -- **GitHub Issues**: [Report bugs or request features](https://github.com/asgardeo/web-ui-sdks/issues) -- **Community**: [Join the discussion](https://github.com/asgardeo/web-ui-sdks/discussions) - -## License - -This project is licensed under the Apache License 2.0. See [LICENSE](../../LICENSE) for details. From 6d3b636f7f1c92bacfd40f338a169c429a7153c9 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 12:19:42 +0530 Subject: [PATCH 12/31] refactor: remove unused import for PoundSterling in UserDropdown component --- .../teamspace-react/src/components/Header/UserDropdown.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/samples/teamspace-react/src/components/Header/UserDropdown.tsx b/samples/teamspace-react/src/components/Header/UserDropdown.tsx index 100237b51..cede91015 100644 --- a/samples/teamspace-react/src/components/Header/UserDropdown.tsx +++ b/samples/teamspace-react/src/components/Header/UserDropdown.tsx @@ -1,8 +1,7 @@ 'use client'; -import {ChevronDown, CogIcon, LogOut, Settings, UserIcon, Workflow, LayoutDashboard} from 'lucide-react'; -import {UserDropdown as _UserDropdown, SignOutButton, UserProfile} from '@asgardeo/react'; -import {PoundSterling} from 'lucide-react'; +import {ChevronDown, LogOut, Settings, UserIcon, Workflow, LayoutDashboard} from 'lucide-react'; +import {UserDropdown as _UserDropdown, SignOutButton, UserProfile} from '@asgardeo/react';; import {useState, useRef} from 'react'; import {Link} from 'react-router-dom'; From 2cb2f27b1bed8211e6d17ac0cec61a046eac3f0d Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 12:44:31 +0530 Subject: [PATCH 13/31] chore(react): integrate vendor CSS class prefixing in Typography component --- .../primitives/Typography/Typography.tsx | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/primitives/Typography/Typography.tsx b/packages/react/src/components/primitives/Typography/Typography.tsx index 473d2f2b9..18d41fe7e 100644 --- a/packages/react/src/components/primitives/Typography/Typography.tsx +++ b/packages/react/src/components/primitives/Typography/Typography.tsx @@ -19,6 +19,7 @@ import {CSSProperties, FC, ReactNode, ComponentPropsWithoutRef, ElementType} from 'react'; import useTheme from '../../../contexts/Theme/useTheme'; import clsx from 'clsx'; +import {withVendorCSSClassPrefix} from '@asgardeo/browser'; // Typography variants mapped to HTML elements and styling export type TypographyVariant = @@ -292,19 +293,21 @@ const Typography: FC = ({ ...style, }; - const classes = clsx( - 'wso2-typography', - `wso2-typography-${variant}`, - { - 'wso2-typography-noWrap': noWrap, - 'wso2-typography-inline': inline, - 'wso2-typography-gutterBottom': gutterBottom, - }, - className, - ); - return ( - + {children} ); From f29ada38160138afe3a1eeb7ed5d71fa659637ef Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 12:45:20 +0530 Subject: [PATCH 14/31] chore(react): replace button elements with Button component for consistency and improved styling --- .../BaseOrganizationProfile.tsx | 86 +++++++------------ 1 file changed, 32 insertions(+), 54 deletions(-) diff --git a/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx b/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx index fc413ba11..2859ce654 100644 --- a/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx +++ b/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx @@ -341,36 +341,6 @@ const BaseOrganizationProfile: FC = ({ }; const styles = useStyles(); - const buttonStyle = useMemo( - () => ({ - padding: `${theme.vars.spacing.unit} calc(${theme.vars.spacing.unit} * 2)`, - margin: theme.vars.spacing.unit, - borderRadius: theme.vars.borderRadius.medium, - border: 'none', - cursor: 'pointer', - fontSize: '0.875rem', - fontWeight: 500, - }), - [theme], - ); - - const saveButtonStyle = useMemo( - () => ({ - ...buttonStyle, - backgroundColor: theme.vars.colors.primary.main, - color: theme.vars.colors.primary.contrastText, - }), - [theme, buttonStyle], - ); - - const cancelButtonStyle = useMemo( - () => ({ - ...buttonStyle, - backgroundColor: theme.vars.colors.secondary.main, - border: `1px solid ${theme.vars.colors.border}`, - }), - [theme, buttonStyle], - ); // Renders individual field in view or edit mode const renderField = ( @@ -486,23 +456,22 @@ const BaseOrganizationProfile: FC = ({ }} > {!hasValue && isFieldEditable && onStartEdit ? ( - + ) : ( displayValue )} @@ -553,33 +522,42 @@ const BaseOrganizationProfile: FC = ({
{isFieldEditing ? ( <> - - + + ) : ( // Only show pencil icon when there's a value hasValue && ( - + ) )}
From d4082d5289f7e795524278a538912c32e05e5c20 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 13:05:15 +0530 Subject: [PATCH 15/31] refactor(react): replace button elements with Button component for consistency and improved styling --- .../BaseOrganizationProfile.tsx | 16 ++-- .../UserProfile/BaseUserProfile.tsx | 84 ++++++------------- 2 files changed, 32 insertions(+), 68 deletions(-) diff --git a/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx b/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx index 2859ce654..bf2cf9a54 100644 --- a/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx +++ b/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx @@ -522,19 +522,19 @@ const BaseOrganizationProfile: FC = ({
{isFieldEditing ? ( <> - - + ) : ( displayValue )} @@ -526,23 +495,22 @@ const BaseUserProfile: FC = ({ {label}
{!hasValue && isEditable && onStartEdit ? ( - + ) : ( displayValue )} @@ -601,12 +569,7 @@ const BaseUserProfile: FC = ({ - @@ -748,10 +711,10 @@ const useStyles = () => { } as CSSProperties, field: { display: 'flex', - alignItems: 'center', - padding: `${theme.vars.spacing.unit} 0`, + alignItems: 'flex-start', + padding: `calc(${theme.vars.spacing.unit} / 2) 0`, borderBottom: `1px solid ${theme.vars.colors.border}`, - minHeight: '32px', + minHeight: '28px', } as CSSProperties, lastField: { borderBottom: 'none', @@ -762,7 +725,7 @@ const useStyles = () => { color: theme.vars.colors.text.secondary, width: '120px', flexShrink: 0, - lineHeight: '32px', + lineHeight: '28px', } as CSSProperties, value: { color: theme.vars.colors.text.primary, @@ -771,12 +734,13 @@ const useStyles = () => { alignItems: 'center', gap: theme.vars.spacing.unit, overflow: 'hidden', - minHeight: '32px', + minHeight: '28px', '& input': { height: '32px', margin: 0, }, - lineHeight: '32px', + lineHeight: '28px', + wordBreak: 'break-word' as const, '& table': { backgroundColor: theme.vars.colors.background.surface, borderRadius: theme.vars.borderRadius.medium, From 12a4e27d417dfc05a994adaef2916d98ebe3599b Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 13:07:22 +0530 Subject: [PATCH 16/31] feat(react): add skeleton loading animation to Avatar component and refactor Divider styles with CSS classes --- .../components/primitives/Avatar/Avatar.tsx | 27 ++- .../components/primitives/Divider/Divider.tsx | 156 +++++++++--------- 2 files changed, 106 insertions(+), 77 deletions(-) diff --git a/packages/react/src/components/primitives/Avatar/Avatar.tsx b/packages/react/src/components/primitives/Avatar/Avatar.tsx index c15d81621..61cec2a3c 100644 --- a/packages/react/src/components/primitives/Avatar/Avatar.tsx +++ b/packages/react/src/components/primitives/Avatar/Avatar.tsx @@ -169,11 +169,36 @@ export const Avatar: FC = ({ if (name) { return getInitials(name); } - return '?'; + + // Skeleton loading animation + return ( +
+ ); }; return (
+ {renderContent()}
); diff --git a/packages/react/src/components/primitives/Divider/Divider.tsx b/packages/react/src/components/primitives/Divider/Divider.tsx index a95f1c63b..94e610a9c 100644 --- a/packages/react/src/components/primitives/Divider/Divider.tsx +++ b/packages/react/src/components/primitives/Divider/Divider.tsx @@ -54,50 +54,45 @@ const useStyles = (orientation: DividerOrientation, variant: DividerVariant, col const baseColor = color || theme.colors.border; const borderStyle = variant === 'solid' ? 'solid' : variant === 'dashed' ? 'dashed' : 'dotted'; - if (orientation === 'vertical') { - return { - container: { - display: 'inline-block', - height: '100%', - minHeight: `calc(${theme.vars.spacing.unit} * 2)`, - width: '1px', - borderLeft: `1px ${borderStyle} ${baseColor}`, - margin: `0 calc(${theme.vars.spacing.unit} * 1)`, - }, - }; - } - - // Horizontal divider - const baseStyle = { - display: 'flex', - alignItems: 'center', - width: '100%', - margin: `calc(${theme.vars.spacing.unit} * 2) 0`, - }; - - if (hasChildren) { - return { - container: baseStyle, - line: { - flex: 1, - height: '1px', - borderTop: `1px ${borderStyle} ${baseColor}`, - }, - text: { - backgroundColor: theme.vars.colors.background.surface, - padding: `0 calc(${theme.vars.spacing.unit} * 1)`, - whiteSpace: 'nowrap' as const, - }, - }; - } - - return { - container: { - ...baseStyle, - height: '1px', - borderTop: `1px ${borderStyle} ${baseColor}`, - }, - }; + const styles = ` + .${withVendorCSSClassPrefix('divider')} { + margin: calc(${theme.vars.spacing.unit} * 2) 0; + } + + .${withVendorCSSClassPrefix('divider--vertical')} { + display: inline-block; + height: 100%; + min-height: calc(${theme.vars.spacing.unit} * 2); + width: 1px; + border-left: 1px ${borderStyle} ${baseColor}; + margin: 0 calc(${theme.vars.spacing.unit} * 1); + } + + .${withVendorCSSClassPrefix('divider--horizontal')} { + display: flex; + align-items: center; + width: 100%; + } + + .${withVendorCSSClassPrefix('divider--horizontal')}:not(.${withVendorCSSClassPrefix('divider--with-text')}) { + height: 1px; + border-top: 1px ${borderStyle} ${baseColor}; + } + + .${withVendorCSSClassPrefix('divider__line')} { + flex: 1; + height: 1px; + border-top: 1px ${borderStyle} ${baseColor}; + } + + .${withVendorCSSClassPrefix('divider__text')} { + background-color: ${theme.vars.colors.background.surface}; + padding: 0 calc(${theme.vars.spacing.unit} * 1); + white-space: nowrap; + } + `; + + return styles; }, [orientation, variant, color, hasChildren, theme]); }; @@ -132,47 +127,56 @@ const Divider: FC = ({ if (orientation === 'vertical') { return ( -
+ <> + +
+ ); } if (children) { return ( -
-
- - {children} - -
-
+ <> + +
+
+ + {children} + +
+
+ ); } return ( -
+ <> + +
+ ); }; From 67b694ba07b901f1cceaf9751986d49c2ea2a403 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 15:08:11 +0530 Subject: [PATCH 17/31] chore: add action color properties to theme configuration and update components to use action colors --- packages/javascript/src/theme/createTheme.ts | 78 +++++++++++++++++++ packages/javascript/src/theme/types.ts | 26 +++++++ .../transformBrandingPreferenceToTheme.ts | 19 ++++- packages/react/docs/OVERVIEW.md | 0 .../BaseOrganizationSwitcher.tsx | 9 ++- .../UserDropdown/BaseUserDropdown.tsx | 6 +- .../components/primitives/Button/Button.tsx | 20 ++--- .../components/primitives/Divider/Divider.tsx | 19 ++++- .../KeyValueInput/KeyValueInput.tsx | 4 +- 9 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 packages/react/docs/OVERVIEW.md diff --git a/packages/javascript/src/theme/createTheme.ts b/packages/javascript/src/theme/createTheme.ts index e13253206..616fbc22c 100644 --- a/packages/javascript/src/theme/createTheme.ts +++ b/packages/javascript/src/theme/createTheme.ts @@ -38,6 +38,19 @@ import VendorConstants from '../constants/VendorConstants'; const lightTheme: ThemeConfig = { colors: { + action: { + active: 'rgba(0, 0, 0, 0.54)', + hover: 'rgba(0, 0, 0, 0.04)', + hoverOpacity: 0.04, + selected: 'rgba(0, 0, 0, 0.08)', + selectedOpacity: 0.08, + disabled: 'rgba(0, 0, 0, 0.26)', + disabledBackground: 'rgba(0, 0, 0, 0.12)', + disabledOpacity: 0.38, + focus: 'rgba(0, 0, 0, 0.12)', + focusOpacity: 0.12, + activatedOpacity: 0.12, + }, primary: { main: '#1a73e8', contrastText: '#ffffff', @@ -111,6 +124,19 @@ const lightTheme: ThemeConfig = { const darkTheme: ThemeConfig = { colors: { + action: { + active: 'rgba(255, 255, 255, 0.70)', + hover: 'rgba(255, 255, 255, 0.04)', + hoverOpacity: 0.04, + selected: 'rgba(255, 255, 255, 0.08)', + selectedOpacity: 0.08, + disabled: 'rgba(255, 255, 255, 0.26)', + disabledBackground: 'rgba(255, 255, 255, 0.12)', + disabledOpacity: 0.38, + focus: 'rgba(255, 255, 255, 0.12)', + focusOpacity: 0.12, + activatedOpacity: 0.12, + }, primary: { main: '#1a73e8', contrastText: '#ffffff', @@ -186,6 +212,41 @@ const toCssVariables = (theme: ThemeConfig): Record => { const cssVars: Record = {}; const prefix = theme.cssVarPrefix || VendorConstants.VENDOR_PREFIX; + // Colors - Action + if (theme.colors?.action?.active) { + cssVars[`--${prefix}-color-action-active`] = theme.colors.action.active; + } + if (theme.colors?.action?.hover) { + cssVars[`--${prefix}-color-action-hover`] = theme.colors.action.hover; + } + if (theme.colors?.action?.hoverOpacity !== undefined) { + cssVars[`--${prefix}-color-action-hoverOpacity`] = theme.colors.action.hoverOpacity.toString(); + } + if (theme.colors?.action?.selected) { + cssVars[`--${prefix}-color-action-selected`] = theme.colors.action.selected; + } + if (theme.colors?.action?.selectedOpacity !== undefined) { + cssVars[`--${prefix}-color-action-selectedOpacity`] = theme.colors.action.selectedOpacity.toString(); + } + if (theme.colors?.action?.disabled) { + cssVars[`--${prefix}-color-action-disabled`] = theme.colors.action.disabled; + } + if (theme.colors?.action?.disabledBackground) { + cssVars[`--${prefix}-color-action-disabledBackground`] = theme.colors.action.disabledBackground; + } + if (theme.colors?.action?.disabledOpacity !== undefined) { + cssVars[`--${prefix}-color-action-disabledOpacity`] = theme.colors.action.disabledOpacity.toString(); + } + if (theme.colors?.action?.focus) { + cssVars[`--${prefix}-color-action-focus`] = theme.colors.action.focus; + } + if (theme.colors?.action?.focusOpacity !== undefined) { + cssVars[`--${prefix}-color-action-focusOpacity`] = theme.colors.action.focusOpacity.toString(); + } + if (theme.colors?.action?.activatedOpacity !== undefined) { + cssVars[`--${prefix}-color-action-activatedOpacity`] = theme.colors.action.activatedOpacity.toString(); + } + // Colors - Primary if (theme.colors?.primary?.main) { cssVars[`--${prefix}-color-primary-main`] = theme.colors.primary.main; @@ -338,6 +399,19 @@ const toThemeVars = (theme: ThemeConfig): ThemeVars => { return { colors: { + action: { + active: `var(--${prefix}-color-action-active)`, + hover: `var(--${prefix}-color-action-hover)`, + hoverOpacity: `var(--${prefix}-color-action-hoverOpacity)`, + selected: `var(--${prefix}-color-action-selected)`, + selectedOpacity: `var(--${prefix}-color-action-selectedOpacity)`, + disabled: `var(--${prefix}-color-action-disabled)`, + disabledBackground: `var(--${prefix}-color-action-disabledBackground)`, + disabledOpacity: `var(--${prefix}-color-action-disabledOpacity)`, + focus: `var(--${prefix}-color-action-focus)`, + focusOpacity: `var(--${prefix}-color-action-focusOpacity)`, + activatedOpacity: `var(--${prefix}-color-action-activatedOpacity)`, + }, primary: { main: `var(--${prefix}-color-primary-main)`, contrastText: `var(--${prefix}-color-primary-contrastText)`, @@ -419,6 +493,10 @@ const createTheme = (config: RecursivePartial = {}, isDark = false) colors: { ...baseTheme.colors, ...config.colors, + action: { + ...baseTheme.colors.action, + ...(config.colors?.action || {}), + }, secondary: { ...baseTheme.colors.secondary, ...(config.colors?.secondary || {}), diff --git a/packages/javascript/src/theme/types.ts b/packages/javascript/src/theme/types.ts index 26a0a9ce0..0bc6e0142 100644 --- a/packages/javascript/src/theme/types.ts +++ b/packages/javascript/src/theme/types.ts @@ -41,6 +41,19 @@ export interface ThemeTypography { } export interface ThemeColors { + action: { + active: string; + hover: string; + hoverOpacity: number; + selected: string; + selectedOpacity: number; + disabled: string; + disabledBackground: string; + disabledOpacity: number; + focus: string; + focusOpacity: number; + activatedOpacity: number; + }; background: { body: { main: string; @@ -122,6 +135,19 @@ export interface ThemeConfig { export interface ThemeVars { colors: { + action: { + active: string; + hover: string; + hoverOpacity: string; + selected: string; + selectedOpacity: string; + disabled: string; + disabledBackground: string; + disabledOpacity: string; + focus: string; + focusOpacity: string; + activatedOpacity: string; + }; primary: { main: string; contrastText: string; diff --git a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts index d875fc7bf..16142a312 100644 --- a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts +++ b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts @@ -37,13 +37,26 @@ const extractContrastText = (colorVariant?: {main?: string; contrastText?: strin /** * Transforms a ThemeVariant from branding preference to ThemeConfig */ -const transformThemeVariant = (themeVariant: ThemeVariant): Partial => { +const transformThemeVariant = (themeVariant: ThemeVariant, isDark = false): Partial => { const colors = themeVariant.colors; const buttons = themeVariant.buttons; const inputs = themeVariant.inputs; return { colors: { + action: { + active: isDark ? 'rgba(255, 255, 255, 0.70)' : 'rgba(0, 0, 0, 0.54)', + hover: isDark ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)', + hoverOpacity: 0.04, + selected: isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)', + selectedOpacity: 0.08, + disabled: isDark ? 'rgba(255, 255, 255, 0.26)' : 'rgba(0, 0, 0, 0.26)', + disabledBackground: isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)', + disabledOpacity: 0.38, + focus: isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)', + focusOpacity: 0.12, + activatedOpacity: 0.12, + }, primary: { main: extractColorValue(colors?.primary), contrastText: extractContrastText(colors?.primary), @@ -130,7 +143,7 @@ export const transformBrandingPreferenceToTheme = ( // If the specified theme variant doesn't exist, fallback to light theme const fallbackVariant = themeConfig.LIGHT || themeConfig.DARK; if (fallbackVariant) { - const transformedConfig = transformThemeVariant(fallbackVariant); + const transformedConfig = transformThemeVariant(fallbackVariant, activeThemeKey === 'DARK'); return createTheme(transformedConfig, activeThemeKey === 'DARK'); } // If no theme variants exist, return default theme @@ -138,7 +151,7 @@ export const transformBrandingPreferenceToTheme = ( } // Transform the theme variant to ThemeConfig - const transformedConfig = transformThemeVariant(themeVariant); + const transformedConfig = transformThemeVariant(themeVariant, activeThemeKey === 'DARK'); // Create the theme using the transformed config return createTheme(transformedConfig, activeThemeKey === 'DARK'); diff --git a/packages/react/docs/OVERVIEW.md b/packages/react/docs/OVERVIEW.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx index 6636e8ebe..548e136ac 100644 --- a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx +++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx @@ -99,6 +99,9 @@ const useStyles = () => { textAlign: 'left', borderRadius: theme.vars.borderRadius.medium, transition: 'background-color 0.15s ease-in-out', + '&:hover': { + backgroundColor: theme.vars.colors.action?.hover || 'rgba(0, 0, 0, 0.04)', + }, } as CSSProperties, organizationInfo: { display: 'flex', @@ -599,7 +602,7 @@ export const BaseOrganizationSwitcher: FC = ({ ...styles.menuItem, backgroundColor: hoveredItemIndex === switchableOrganizations.indexOf(organization) - ? theme.vars.colors.secondary.main + ? theme.vars.colors.action?.hover : 'transparent', }} onMouseEnter={(): void => setHoveredItemIndex(switchableOrganizations.indexOf(organization))} @@ -629,7 +632,7 @@ export const BaseOrganizationSwitcher: FC = ({ ...styles.menuItem, backgroundColor: hoveredItemIndex === switchableOrganizations.length + index - ? theme.vars.colors.secondary.main + ? theme.vars.colors.action?.hover : 'transparent', }} className={withVendorCSSClassPrefix('organization-switcher__menu-item')} @@ -648,7 +651,7 @@ export const BaseOrganizationSwitcher: FC = ({ ...styles.menuItem, backgroundColor: hoveredItemIndex === switchableOrganizations.length + index - ? theme.vars.colors.secondary.main + ? theme.vars.colors.action?.hover : 'transparent', }} className={withVendorCSSClassPrefix('organization-switcher__menu-item')} diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index a3033831d..39515fdf0 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -407,8 +407,7 @@ export const BaseUserDropdown: FC = ({ href={item.href} style={{ ...styles.menuItemAnchor, - backgroundColor: - hoveredItemIndex === index ? theme.vars.colors.secondary.main : 'transparent', + backgroundColor: hoveredItemIndex === index ? theme.vars.colors.action?.hover : 'transparent', }} className={withVendorCSSClassPrefix('user-dropdown__menu-item')} onMouseEnter={() => setHoveredItemIndex(index)} @@ -424,8 +423,7 @@ export const BaseUserDropdown: FC = ({ onClick={() => handleMenuItemClick(item)} style={{ ...styles.menuItem, - backgroundColor: - hoveredItemIndex === index ? theme.vars.colors.secondary.main : 'transparent', + backgroundColor: hoveredItemIndex === index ? theme.vars.colors.action?.hover : 'transparent', }} className={withVendorCSSClassPrefix('user-dropdown__menu-item')} color="tertiary" diff --git a/packages/react/src/components/primitives/Button/Button.tsx b/packages/react/src/components/primitives/Button/Button.tsx index 5c5c88e4e..22301bd83 100644 --- a/packages/react/src/components/primitives/Button/Button.tsx +++ b/packages/react/src/components/primitives/Button/Button.tsx @@ -127,11 +127,10 @@ const useButtonStyles = ( color: theme.vars.colors.primary.main, border: '1px solid transparent', '&:hover': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.hover, }, '&:active': { - backgroundColor: theme.vars.colors.background.surface, - opacity: 0.8, + backgroundColor: theme.vars.colors.action.selected, }, }; } @@ -173,11 +172,10 @@ const useButtonStyles = ( color: theme.vars.colors.secondary.main, border: '1px solid transparent', '&:hover': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.hover, }, '&:active': { - backgroundColor: theme.vars.colors.background.surface, - opacity: 0.8, + backgroundColor: theme.vars.colors.action.selected, }, }; } @@ -205,13 +203,12 @@ const useButtonStyles = ( color: theme.vars.colors.text.secondary, border: `1px solid ${theme.vars.colors.border}`, '&:hover': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.hover, borderColor: theme.vars.colors.text.secondary, }, '&:active': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.selected, borderColor: theme.vars.colors.text.primary, - opacity: 0.9, }, }; case 'text': @@ -220,13 +217,12 @@ const useButtonStyles = ( color: theme.vars.colors.text.secondary, border: '1px solid transparent', '&:hover': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.hover, color: theme.vars.colors.text.primary, }, '&:active': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.selected, color: theme.vars.colors.text.primary, - opacity: 0.8, }, }; } diff --git a/packages/react/src/components/primitives/Divider/Divider.tsx b/packages/react/src/components/primitives/Divider/Divider.tsx index 94e610a9c..4059591a1 100644 --- a/packages/react/src/components/primitives/Divider/Divider.tsx +++ b/packages/react/src/components/primitives/Divider/Divider.tsx @@ -130,7 +130,11 @@ const Divider: FC = ({ <>
= ({ {...rest} >
- + {children}
@@ -170,7 +179,11 @@ const Divider: FC = ({ <>
= ({ backgroundColor: 'transparent', border: 'none', '&:hover': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.hover, }, } as CSSProperties, pairInput: { @@ -290,7 +290,7 @@ const KeyValueInput: FC = ({ alignItems: 'center', justifyContent: 'center', '&:hover': { - backgroundColor: theme.vars.colors.background.surface, + backgroundColor: theme.vars.colors.action.hover, color: theme.vars.colors.error.main, }, } as CSSProperties, From 3be586f62acda10c00b36dbba5cb3d214e83dd13 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 15:16:01 +0530 Subject: [PATCH 18/31] feat(react): add error alert to BaseCreateOrganization component for improved user feedback --- .../BaseCreateOrganization.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx index 81a5a98e4..1a50c46bb 100644 --- a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx @@ -21,6 +21,7 @@ import clsx from 'clsx'; import {ChangeEvent, CSSProperties, FC, ReactElement, ReactNode, useMemo, useState} from 'react'; import useTheme from '../../../contexts/Theme/useTheme'; import useTranslation from '../../../hooks/useTranslation'; +import Alert from '../../primitives/Alert/Alert'; import Button from '../../primitives/Button/Button'; import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; import FormControl from '../../primitives/FormControl/FormControl'; @@ -284,6 +285,17 @@ export const BaseCreateOrganization: FC = ({ style={styles.form} onSubmit={handleSubmit} > + {/* Error Alert */} + {error && ( + + Error + {error} + + )} + {/* Organization Name */}
= ({ {/* Additional Fields */} {renderAdditionalFields && renderAdditionalFields()} - - {/* Error Message */} - {error && ( - - {error} - - )} {/* Actions */} From 22b77733b63cc22b2e158e497fd39f8c629332b0 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 15:36:01 +0530 Subject: [PATCH 19/31] feat(react): integrate extractUserClaimsFromIdToken for improved user profile handling and simplify error alert rendering --- packages/nextjs/src/AsgardeoNextClient.ts | 5 +- packages/react/src/AsgardeoReactClient.ts | 13 +-- .../BaseCreateOrganization.tsx | 5 +- .../BaseOrganizationSwitcher.tsx | 1 - .../UserProfile/BaseUserProfile.tsx | 89 +++++++++++++------ 5 files changed, 73 insertions(+), 40 deletions(-) diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index ae6015439..84602143b 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -49,6 +49,7 @@ import { deriveOrganizationHandleFromBaseUrl, getAllOrganizations, AllOrganizationsApiResponse, + extractUserClaimsFromIdToken, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; @@ -200,8 +201,8 @@ class AsgardeoNextClient exte } catch (error) { return { schemas: [], - flattenedProfile: await this.asgardeo.getDecodedIdToken(userId), - profile: await this.asgardeo.getDecodedIdToken(userId), + flattenedProfile: extractUserClaimsFromIdToken(await this.asgardeo.getDecodedIdToken(userId)), + profile: extractUserClaimsFromIdToken(await this.asgardeo.getDecodedIdToken(userId)), }; } } diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 98c5f9356..d41f0fc2f 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -37,6 +37,7 @@ import { EmbeddedFlowExecuteRequestConfig, deriveOrganizationHandleFromBaseUrl, AllOrganizationsApiResponse, + extractUserClaimsFromIdToken, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import getMeOrganizations from './api/getMeOrganizations'; @@ -89,7 +90,7 @@ class AsgardeoReactClient e return generateUserProfile(profile, flattenUserSchema(schemas)); } catch (error) { - return this.asgardeo.getDecodedIdToken(); + return extractUserClaimsFromIdToken(await this.getDecodedIdToken()); } } @@ -121,8 +122,8 @@ class AsgardeoReactClient e } catch (error) { return { schemas: [], - flattenedProfile: await this.asgardeo.getDecodedIdToken(), - profile: await this.asgardeo.getDecodedIdToken(), + flattenedProfile: extractUserClaimsFromIdToken(await this.getDecodedIdToken()), + profile: extractUserClaimsFromIdToken(await this.getDecodedIdToken()), }; } } @@ -139,7 +140,9 @@ class AsgardeoReactClient e return getMeOrganizations({baseUrl}); } catch (error) { throw new AsgardeoRuntimeError( - `Failed to fetch the user's associated organizations: ${error instanceof Error ? error.message : String(error)}`, + `Failed to fetch the user's associated organizations: ${ + error instanceof Error ? error.message : String(error) + }`, 'AsgardeoReactClient-getMyOrganizations-RuntimeError-001', 'react', 'An error occurred while fetching associated organizations of the signed-in user.', @@ -168,7 +171,7 @@ class AsgardeoReactClient e } override async getCurrentOrganization(): Promise { - const idToken: IdToken = await this.asgardeo.getDecodedIdToken(); + const idToken: IdToken = await this.getDecodedIdToken(); return { orgHandle: idToken?.org_handle, diff --git a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx index 1a50c46bb..30a6b194a 100644 --- a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx @@ -287,10 +287,7 @@ export const BaseCreateOrganization: FC = ({ > {/* Error Alert */} {error && ( - + Error {error} diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx index 548e136ac..4b21ce51d 100644 --- a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx +++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx @@ -137,7 +137,6 @@ const useStyles = () => { alignItems: 'center', gap: theme.vars.spacing.unit, padding: `${theme.vars.spacing.unit} calc(${theme.vars.spacing.unit} * 2)`, - borderBottom: `1px solid ${theme.vars.colors.border}`, } as CSSProperties, loadingContainer: { display: 'flex', diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx index 6d498cea7..bb9c4c15c 100644 --- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx @@ -106,7 +106,7 @@ const BaseUserProfile: FC = ({ className = '', cardLayout = true, profile, - schemas, + schemas = [], flattenedProfile, mode = 'inline', title = 'User Profile', @@ -618,6 +618,33 @@ const BaseUserProfile: FC = ({ const avatarAttributes = ['picture']; const excludedProps = avatarAttributes.map(attr => mergedMappings[attr] || attr); + // Function to render profile fields when schemas are not available + const renderProfileWithoutSchemas = () => { + if (!currentUser) return null; + + const profileEntries = Object.entries(currentUser) + .filter(([key, value]) => { + // Skip fields that are in the fieldsToSkip array + if (fieldsToSkip.includes(key)) return false; + // Skip empty values + return value !== undefined && value !== '' && value !== null; + }) + .sort(([a], [b]) => a.localeCompare(b)); // Sort alphabetically + + return ( + <> + {profileEntries.map(([key, value]) => ( +
+ {formatLabel(key)} +
+ {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} +
+
+ ))} + + ); + }; + const profileContent = (
@@ -629,34 +656,40 @@ const BaseUserProfile: FC = ({ />
- {schemas - .filter(schema => { - // Skip fields that are in the fieldsToSkip array - if (fieldsToSkip.includes(schema.name)) return false; - - // For non-editable mode, only show fields with values - if (!editable) { + {schemas && schemas.length > 0 ? ( + // Render with schemas when available + schemas + .filter(schema => { + // Skip fields that are in the fieldsToSkip array + if (fieldsToSkip.includes(schema.name)) return false; + + // For non-editable mode, only show fields with values + if (!editable) { + const value = flattenedProfile && schema.name ? flattenedProfile[schema.name] : undefined; + return value !== undefined && value !== '' && value !== null; + } + + return true; + }) + .sort((a, b) => { + const orderA = a.displayOrder ? parseInt(a.displayOrder) : 999; + const orderB = b.displayOrder ? parseInt(b.displayOrder) : 999; + return orderA - orderB; + }) + .map((schema, index) => { + // Get the value from flattenedProfile const value = flattenedProfile && schema.name ? flattenedProfile[schema.name] : undefined; - return value !== undefined && value !== '' && value !== null; - } - - return true; - }) - .sort((a, b) => { - const orderA = a.displayOrder ? parseInt(a.displayOrder) : 999; - const orderB = b.displayOrder ? parseInt(b.displayOrder) : 999; - return orderA - orderB; - }) - .map((schema, index) => { - // Get the value from flattenedProfile - const value = flattenedProfile && schema.name ? flattenedProfile[schema.name] : undefined; - const schemaWithValue = { - ...schema, - value, - }; - - return
{renderUserInfo(schemaWithValue)}
; - })} + const schemaWithValue = { + ...schema, + value, + }; + + return
{renderUserInfo(schemaWithValue)}
; + }) + ) : ( + // Fallback: render profile fields directly when schemas are not available + renderProfileWithoutSchemas() + )}
); From d5a08b840f849f8ebc110c181ce63094755ee28f Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 15:41:54 +0530 Subject: [PATCH 20/31] chore(react): enhance username processing in processUsername function to handle multiple field variations and improve code readability style(react): adjust dropdown dimensions in BaseUserDropdown for better UI consistency refactor(react): simplify rendering logic in BaseUserProfile component for improved clarity and maintainability --- .../javascript/src/utils/processUsername.ts | 41 ++++++++--- .../UserDropdown/BaseUserDropdown.tsx | 4 +- .../UserProfile/BaseUserProfile.tsx | 68 +++++++++---------- 3 files changed, 64 insertions(+), 49 deletions(-) diff --git a/packages/javascript/src/utils/processUsername.ts b/packages/javascript/src/utils/processUsername.ts index 315210add..8826efc20 100644 --- a/packages/javascript/src/utils/processUsername.ts +++ b/packages/javascript/src/utils/processUsername.ts @@ -58,11 +58,12 @@ export const removeUserstorePrefix = (username?: string): string => { }; /** - * Processes a user object to remove userstore prefixes from the username field. + * Processes a user object to remove userstore prefixes from username fields. * This is a helper function for processing user objects returned from SCIM2 endpoints. + * Handles various username field variations: username, userName, and user_name. * * @param user - The user object to process - * @returns The user object with the processed username + * @returns The user object with processed username fields * * @example * ```typescript @@ -70,20 +71,38 @@ export const removeUserstorePrefix = (username?: string): string => { * const processedUser = processUserUsername(user); * console.log(processedUser.username); // "john.doe" * - * const asgardeoUser = { username: "ASGARDEO_USER/jane.doe", email: "jane@example.com" }; - * const processedAsgardeoUser = processUserUsername(asgardeoUser); - * console.log(processedAsgardeoUser.username); // "jane.doe" + * const camelCaseUser = { userName: "ASGARDEO_USER/jane.doe", email: "jane@example.com" }; + * const processedCamelCaseUser = processUserUsername(camelCaseUser); + * console.log(processedCamelCaseUser.userName); // "jane.doe" + * + * const snakeCaseUser = { user_name: "PRIMARY/admin", email: "admin@example.com" }; + * const processedSnakeCaseUser = processUserUsername(snakeCaseUser); + * console.log(processedSnakeCaseUser.user_name); // "admin" * ``` */ -const processUsername = (user: T): T => { - if (!user || !user.username) { +const processUsername = (user: T): T => { + if (!user) { return user; } - return { - ...user, - username: removeUserstorePrefix(user.username), - }; + const processedUser = {...user}; + + // Process username field + if (processedUser.username) { + processedUser.username = removeUserstorePrefix(processedUser.username); + } + + // Process userName field + if (processedUser.userName) { + processedUser.userName = removeUserstorePrefix(processedUser.userName); + } + + // Process user_name field + if (processedUser.user_name) { + processedUser.user_name = removeUserstorePrefix(processedUser.user_name); + } + + return processedUser; }; export default processUsername; diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 39515fdf0..83a3a7fa2 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -67,8 +67,8 @@ const useStyles = () => { maxWidth: '120px', } as CSSProperties, dropdownContent: { - minWidth: '200px', - maxWidth: '300px', + minWidth: '270px', + maxWidth: '500px', backgroundColor: theme.vars.colors.background.surface, borderRadius: theme.vars.borderRadius.medium, boxShadow: theme.vars.shadows.medium, diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx index bb9c4c15c..32ecaf570 100644 --- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.tsx @@ -636,9 +636,7 @@ const BaseUserProfile: FC = ({ {profileEntries.map(([key, value]) => (
{formatLabel(key)} -
- {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} -
+
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
))} @@ -656,40 +654,38 @@ const BaseUserProfile: FC = ({ />
- {schemas && schemas.length > 0 ? ( - // Render with schemas when available - schemas - .filter(schema => { - // Skip fields that are in the fieldsToSkip array - if (fieldsToSkip.includes(schema.name)) return false; - - // For non-editable mode, only show fields with values - if (!editable) { + {schemas && schemas.length > 0 + ? // Render with schemas when available + schemas + .filter(schema => { + // Skip fields that are in the fieldsToSkip array + if (fieldsToSkip.includes(schema.name)) return false; + + // For non-editable mode, only show fields with values + if (!editable) { + const value = flattenedProfile && schema.name ? flattenedProfile[schema.name] : undefined; + return value !== undefined && value !== '' && value !== null; + } + + return true; + }) + .sort((a, b) => { + const orderA = a.displayOrder ? parseInt(a.displayOrder) : 999; + const orderB = b.displayOrder ? parseInt(b.displayOrder) : 999; + return orderA - orderB; + }) + .map((schema, index) => { + // Get the value from flattenedProfile const value = flattenedProfile && schema.name ? flattenedProfile[schema.name] : undefined; - return value !== undefined && value !== '' && value !== null; - } - - return true; - }) - .sort((a, b) => { - const orderA = a.displayOrder ? parseInt(a.displayOrder) : 999; - const orderB = b.displayOrder ? parseInt(b.displayOrder) : 999; - return orderA - orderB; - }) - .map((schema, index) => { - // Get the value from flattenedProfile - const value = flattenedProfile && schema.name ? flattenedProfile[schema.name] : undefined; - const schemaWithValue = { - ...schema, - value, - }; - - return
{renderUserInfo(schemaWithValue)}
; - }) - ) : ( - // Fallback: render profile fields directly when schemas are not available - renderProfileWithoutSchemas() - )} + const schemaWithValue = { + ...schema, + value, + }; + + return
{renderUserInfo(schemaWithValue)}
; + }) + : // Fallback: render profile fields directly when schemas are not available + renderProfileWithoutSchemas()}
); From 9a6c1527b85f90e63250e8401b1e52c217b17ee2 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 16:46:59 +0530 Subject: [PATCH 21/31] chore(react): remove SignOutButton from AuthenticatedActions component and clean up imports --- .../components/Header/AuthenticatedActions.tsx | 3 +-- samples/teamspace-react/src/main.tsx | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx b/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx index 0d4821511..df9f99a9d 100644 --- a/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx +++ b/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx @@ -1,4 +1,4 @@ -import {SignOutButton, UserDropdown, OrganizationSwitcher} from '@asgardeo/nextjs'; +import {UserDropdown, OrganizationSwitcher} from '@asgardeo/nextjs'; interface AuthenticatedActionsProps { className?: string; @@ -9,7 +9,6 @@ export default function AuthenticatedActions({className = ''}: AuthenticatedActi
-
); } diff --git a/samples/teamspace-react/src/main.tsx b/samples/teamspace-react/src/main.tsx index 826216fec..0c156edb3 100644 --- a/samples/teamspace-react/src/main.tsx +++ b/samples/teamspace-react/src/main.tsx @@ -15,14 +15,14 @@ createRoot(document.getElementById('root')!).render( scopes="openid address email profile user:email read:user internal_organization_create internal_organization_view internal_organization_update internal_organization_delete internal_org_organization_update internal_org_organization_create internal_org_organization_view internal_org_organization_delete" preferences={{ theme: { - overrides: { - colors: { - primary: { - main: '#1976d2', // Custom primary color - contrastText: 'white', - }, - }, - }, + // overrides: { + // colors: { + // primary: { + // main: '#1976d2', // Custom primary color + // contrastText: 'white', + // }, + // }, + // }, mode: 'light', // This will detect theme based on CSS classes // You can also use other modes: // mode: 'system', // Follows system preference (prefers-color-scheme) From 8039e9229a09b6c51241196bfdca46625aed65ca Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 17:02:30 +0530 Subject: [PATCH 22/31] feat(react): implement branding context and provider for enhanced branding management --- .../src/api/getBrandingPreference.ts | 2 +- .../contexts/Asgardeo/AsgardeoProvider.tsx | 121 +++++++++-- .../src/contexts/Branding/BrandingContext.ts | 58 ++++++ .../contexts/Branding/BrandingProvider.tsx | 158 +++++++++++++++ .../contexts/Branding/useBrandingContext.ts | 54 +++++ .../src/contexts/Theme/ThemeProvider.tsx | 52 ++--- packages/react/src/hooks/useBranding.ts | 189 ++++-------------- packages/react/src/index.ts | 9 + 8 files changed, 444 insertions(+), 199 deletions(-) create mode 100644 packages/react/src/contexts/Branding/BrandingContext.ts create mode 100644 packages/react/src/contexts/Branding/BrandingProvider.tsx create mode 100644 packages/react/src/contexts/Branding/useBrandingContext.ts diff --git a/packages/javascript/src/api/getBrandingPreference.ts b/packages/javascript/src/api/getBrandingPreference.ts index 8a040394f..a0f52c5be 100644 --- a/packages/javascript/src/api/getBrandingPreference.ts +++ b/packages/javascript/src/api/getBrandingPreference.ts @@ -134,7 +134,7 @@ const getBrandingPreference = async ({ ); const fetchFn = fetcher || fetch; - const resolvedUrl = `${baseUrl}/api/server/v1/branding-preference${ + const resolvedUrl = `${baseUrl}/api/server/v1/branding-preference/resolve${ queryParams.toString() ? `?${queryParams.toString()}` : '' }`; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index b872f5f41..0a412568b 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -26,8 +26,11 @@ import { SignOutOptions, User, UserProfile, + getBrandingPreference, + GetBrandingPreferenceConfig, + BrandingPreference, } from '@asgardeo/browser'; -import {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState} from 'react'; +import {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState, useCallback} from 'react'; import AsgardeoContext from './AsgardeoContext'; import AsgardeoReactClient from '../../AsgardeoReactClient'; import useBrowserUrl from '../../hooks/useBrowserUrl'; @@ -36,6 +39,7 @@ import FlowProvider from '../Flow/FlowProvider'; import I18nProvider from '../I18n/I18nProvider'; import OrganizationProvider from '../Organization/OrganizationProvider'; import ThemeProvider from '../Theme/ThemeProvider'; +import BrandingProvider from '../Branding/BrandingProvider'; import UserProvider from '../User/UserProvider'; /** @@ -82,9 +86,21 @@ const AsgardeoProvider: FC> = ({ ...rest, }); + // Branding state + const [brandingPreference, setBrandingPreference] = useState(null); + const [isBrandingLoading, setIsBrandingLoading] = useState(false); + const [brandingError, setBrandingError] = useState(null); + const [hasFetchedBranding, setHasFetchedBranding] = useState(false); + useEffect(() => { setBaseUrl(_baseUrl); - }, [_baseUrl]); + // Reset branding state when baseUrl changes + if (_baseUrl !== baseUrl) { + setHasFetchedBranding(false); + setBrandingPreference(null); + setBrandingError(null); + } + }, [_baseUrl, baseUrl]); useEffect(() => { (async (): Promise => { @@ -200,6 +216,63 @@ const AsgardeoProvider: FC> = ({ setMyOrganizations(await asgardeo.getMyOrganizations()); }; + // Branding fetch function + const fetchBranding = useCallback(async (): Promise => { + if (!baseUrl) { + return; + } + + // Prevent multiple calls if already fetching + if (isBrandingLoading) { + return; + } + + setIsBrandingLoading(true); + setBrandingError(null); + + try { + const getBrandingConfig: GetBrandingPreferenceConfig = { + baseUrl, + locale: preferences?.i18n?.language, + // Add other branding config options as needed + }; + + const brandingData = await getBrandingPreference(getBrandingConfig); + setBrandingPreference(brandingData); + setHasFetchedBranding(true); + } catch (err) { + const errorMessage = err instanceof Error ? err : new Error('Failed to fetch branding preference'); + setBrandingError(errorMessage); + setBrandingPreference(null); + setHasFetchedBranding(true); // Mark as fetched even on error to prevent retries + } finally { + setIsBrandingLoading(false); + } + }, [baseUrl, preferences?.i18n?.language]); + + // Refetch branding function + const refetchBranding = useCallback(async (): Promise => { + setHasFetchedBranding(false); // Reset the flag to allow refetching + await fetchBranding(); + }, [fetchBranding]); + + // Auto-fetch branding when initialized and configured + useEffect(() => { + // Enable branding by default or when explicitly enabled + const shouldFetchBranding = preferences?.theme?.inheritFromBranding !== false; + + if (shouldFetchBranding && isInitializedSync && baseUrl && !hasFetchedBranding && !isBrandingLoading) { + fetchBranding(); + } + }, [ + preferences?.theme?.inheritFromBranding, + isInitializedSync, + baseUrl, + hasFetchedBranding, + isBrandingLoading, + fetchBranding, + ]); + const signIn = async (...args: any): Promise => { try { const response: User = await asgardeo.signIn(...args); @@ -283,25 +356,33 @@ const AsgardeoProvider: FC> = ({ }} > - - - - await asgardeo.getAllOrganizations()} - myOrganizations={myOrganizations} - currentOrganization={currentOrganization} - onOrganizationSwitch={switchOrganization} - revalidateMyOrganizations={async () => await asgardeo.getMyOrganizations()} - > - {children} - - - - + + + + await asgardeo.getAllOrganizations()} + myOrganizations={myOrganizations} + currentOrganization={currentOrganization} + onOrganizationSwitch={switchOrganization} + revalidateMyOrganizations={async () => await asgardeo.getMyOrganizations()} + > + {children} + + + + + ); diff --git a/packages/react/src/contexts/Branding/BrandingContext.ts b/packages/react/src/contexts/Branding/BrandingContext.ts new file mode 100644 index 000000000..057d07ff2 --- /dev/null +++ b/packages/react/src/contexts/Branding/BrandingContext.ts @@ -0,0 +1,58 @@ +/** + * 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. + */ + +import {createContext} from 'react'; +import {BrandingPreference, Theme} from '@asgardeo/browser'; + +export interface BrandingContextValue { + /** + * The raw branding preference data + */ + brandingPreference: BrandingPreference | null; + /** + * The transformed theme object + */ + theme: Theme | null; + /** + * The active theme mode from branding preference ('light' | 'dark') + */ + activeTheme: 'light' | 'dark' | null; + /** + * Loading state + */ + isLoading: boolean; + /** + * Error state + */ + error: Error | null; + /** + * Function to manually fetch branding preference + */ + fetchBranding: () => Promise; + /** + * Function to refetch branding preference + * This bypasses the single-call restriction and forces a new API call + */ + refetch: () => Promise; +} + +const BrandingContext = createContext(null); + +BrandingContext.displayName = 'BrandingContext'; + +export default BrandingContext; diff --git a/packages/react/src/contexts/Branding/BrandingProvider.tsx b/packages/react/src/contexts/Branding/BrandingProvider.tsx new file mode 100644 index 000000000..209b06bf8 --- /dev/null +++ b/packages/react/src/contexts/Branding/BrandingProvider.tsx @@ -0,0 +1,158 @@ +/** + * 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. + */ + +import {FC, PropsWithChildren, ReactElement, useCallback, useEffect, useState} from 'react'; +import {BrandingPreference, Theme, transformBrandingPreferenceToTheme} from '@asgardeo/browser'; +import BrandingContext from './BrandingContext'; + +/** + * Configuration options for the BrandingProvider + */ +export interface BrandingProviderProps { + /** + * The branding preference data passed from parent (typically AsgardeoProvider) + */ + brandingPreference?: BrandingPreference | null; + /** + * Force a specific theme ('light' or 'dark') + * If not provided, will use the activeTheme from branding preference + */ + forceTheme?: 'light' | 'dark'; + /** + * Whether the branding provider is enabled + * @default true + */ + enabled?: boolean; + /** + * Loading state passed from parent + */ + isLoading?: boolean; + /** + * Error state passed from parent + */ + error?: Error | null; + /** + * Function to refetch branding preference passed from parent + */ + refetch?: () => Promise; +} + +/** + * BrandingProvider component that manages branding state and provides branding context to child components. + * + * This provider receives branding preferences from a parent component (typically AsgardeoProvider) + * and transforms them into theme objects, making them available to all child components. + * + * Features: + * - Receives branding preferences as props + * - Theme transformation from branding preferences + * - Loading and error states + * - Support for custom theme forcing + * + * @example + * Basic usage (typically used within AsgardeoProvider): + * ```tsx + * + * + * + * ``` + * + * @example + * With custom theme forcing: + * ```tsx + * + * + * + * ``` + */ +const BrandingProvider: FC> = ({ + children, + brandingPreference: externalBrandingPreference, + forceTheme, + enabled = true, + isLoading: externalIsLoading = false, + error: externalError = null, + refetch: externalRefetch, +}: PropsWithChildren): ReactElement => { + const [theme, setTheme] = useState(null); + const [activeTheme, setActiveTheme] = useState<'light' | 'dark' | null>(null); + + // Process branding preference when it changes + useEffect(() => { + if (!enabled || !externalBrandingPreference) { + setTheme(null); + setActiveTheme(null); + return; + } + + // Extract active theme from branding preference + const activeThemeFromBranding = externalBrandingPreference?.preference?.theme?.activeTheme; + let extractedActiveTheme: 'light' | 'dark' | null = null; + + if (activeThemeFromBranding) { + // Convert to lowercase and map to our expected values + const themeMode = activeThemeFromBranding.toLowerCase(); + if (themeMode === 'light' || themeMode === 'dark') { + extractedActiveTheme = themeMode; + } + } + + setActiveTheme(extractedActiveTheme); + + // Transform branding preference to theme + const transformedTheme = transformBrandingPreferenceToTheme(externalBrandingPreference, forceTheme); + setTheme(transformedTheme); + }, [externalBrandingPreference, forceTheme, enabled]); + + // Reset state when disabled + useEffect(() => { + if (!enabled) { + setTheme(null); + setActiveTheme(null); + } + }, [enabled]); + + // Dummy fetchBranding for backward compatibility + const fetchBranding = useCallback(async (): Promise => { + if (externalRefetch) { + await externalRefetch(); + } + }, [externalRefetch]); + + const value = { + brandingPreference: externalBrandingPreference || null, + theme, + activeTheme, + isLoading: externalIsLoading, + error: externalError, + fetchBranding, + refetch: externalRefetch || fetchBranding, + }; + + return {children}; +}; + +export default BrandingProvider; diff --git a/packages/react/src/contexts/Branding/useBrandingContext.ts b/packages/react/src/contexts/Branding/useBrandingContext.ts new file mode 100644 index 000000000..5dc1a03ce --- /dev/null +++ b/packages/react/src/contexts/Branding/useBrandingContext.ts @@ -0,0 +1,54 @@ +/** + * 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. + */ + +import {useContext} from 'react'; +import BrandingContext, {BrandingContextValue} from './BrandingContext'; + +/** + * Hook to access the branding context. + * This hook provides access to branding preferences, theme data, and loading states. + * + * @returns The branding context value containing branding preference data, theme, and control functions + * @throws Error if used outside of a BrandingProvider + * + * @example + * ```tsx + * function MyComponent() { + * const { theme, activeTheme, isLoading, error } = useBrandingContext(); + * + * if (isLoading) return
Loading branding...
; + * if (error) return
Error: {error.message}
; + * + * return ( + *
+ *

Active theme mode: {activeTheme}

+ *

Styled with Asgardeo branding

+ *
+ * ); + * } + * ``` + */ +const useBrandingContext = (): BrandingContextValue => { + const context = useContext(BrandingContext); + if (!context) { + throw new Error('useBrandingContext must be used within a BrandingProvider'); + } + return context; +}; + +export default useBrandingContext; diff --git a/packages/react/src/contexts/Theme/ThemeProvider.tsx b/packages/react/src/contexts/Theme/ThemeProvider.tsx index 6880202ae..cc23970a0 100644 --- a/packages/react/src/contexts/Theme/ThemeProvider.tsx +++ b/packages/react/src/contexts/Theme/ThemeProvider.tsx @@ -30,7 +30,7 @@ import { ThemePreferences, } from '@asgardeo/browser'; import ThemeContext from './ThemeContext'; -import useBranding, {UseBrandingConfig} from '../../hooks/useBranding'; +import useBrandingContext from '../Branding/useBrandingContext'; export interface ThemeProviderProps { theme?: RecursivePartial; @@ -51,10 +51,6 @@ export interface ThemeProviderProps { * Configuration for branding integration */ inheritFromBranding?: ThemePreferences['inheritFromBranding']; - /** - * Configuration for branding API call - */ - brandingConfig?: UseBrandingConfig; } const applyThemeToDOM = (theme: Theme) => { @@ -109,20 +105,6 @@ const applyThemeToDOM = (theme: Theme) => { * * * ``` - * - * @example - * With custom branding configuration: - * ```tsx - * - * - * - * ``` */ const ThemeProvider: FC> = ({ children, @@ -130,7 +112,6 @@ const ThemeProvider: FC> = ({ mode = 'system', detection = {}, inheritFromBranding = true, - brandingConfig, }: PropsWithChildren): ReactElement => { const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(() => { // Initialize with detected theme mode or fallback to defaultMode @@ -145,16 +126,27 @@ const ThemeProvider: FC> = ({ }); // Use branding theme if inheritFromBranding is enabled - const { - theme: brandingTheme, - activeTheme: brandingActiveTheme, - isLoading: isBrandingLoading, - error: brandingError - } = useBranding({ - autoFetch: inheritFromBranding, - // Don't pass forceTheme initially, let branding determine the active theme - ...brandingConfig, // Allow override of branding configuration - }); + // Handle case where BrandingProvider might not be available + let brandingTheme = null; + let brandingActiveTheme = null; + let isBrandingLoading = false; + let brandingError = null; + + try { + const brandingContext = useBrandingContext(); + brandingTheme = brandingContext.theme; + brandingActiveTheme = brandingContext.activeTheme; + isBrandingLoading = brandingContext.isLoading; + brandingError = brandingContext.error; + } catch (error) { + // BrandingProvider not available, fall back to no branding + if (inheritFromBranding) { + console.warn( + 'ThemeProvider: inheritFromBranding is enabled but BrandingProvider is not available. ' + + 'Make sure to wrap your app with BrandingProvider or AsgardeoProvider with branding preferences.', + ); + } + } // Update color scheme based on branding active theme when available useEffect(() => { diff --git a/packages/react/src/hooks/useBranding.ts b/packages/react/src/hooks/useBranding.ts index 7cbe6ae67..a910e1ffc 100644 --- a/packages/react/src/hooks/useBranding.ts +++ b/packages/react/src/hooks/useBranding.ts @@ -16,45 +16,36 @@ * under the License. */ -import {useCallback, useEffect, useState} from 'react'; -import { - getBrandingPreference, - GetBrandingPreferenceConfig, - BrandingPreference, - Theme, - transformBrandingPreferenceToTheme, -} from '@asgardeo/browser'; -import useAsgardeo from '../contexts/Asgardeo/useAsgardeo'; +import {BrandingPreference, Theme} from '@asgardeo/browser'; +import useBrandingContext from '../contexts/Branding/useBrandingContext'; /** * Configuration options for the useBranding hook + * @deprecated Use BrandingProvider instead for better performance and consistency */ export interface UseBrandingConfig { /** - * Locale for the branding preference + * @deprecated This configuration is now handled by BrandingProvider */ locale?: string; /** - * Name of the branding preference + * @deprecated This configuration is now handled by BrandingProvider */ name?: string; /** - * Type of the branding preference + * @deprecated This configuration is now handled by BrandingProvider */ type?: string; /** - * Force a specific theme ('light' or 'dark') - * If not provided, will use the activeTheme from branding preference + * @deprecated This configuration is now handled by BrandingProvider */ forceTheme?: 'light' | 'dark'; /** - * Whether to automatically fetch branding preference on mount - * @default true + * @deprecated This configuration is now handled by BrandingProvider */ autoFetch?: boolean; /** - * Optional custom fetcher function. - * If not provided, native fetch will be used + * @deprecated This configuration is now handled by BrandingProvider */ fetcher?: (url: string, config: RequestInit) => Promise; } @@ -95,14 +86,13 @@ export interface UseBrandingReturn { } /** - * React hook for fetching and transforming branding preferences from Asgardeo. - * This hook automatically fetches branding preferences using the configured - * base URL from the Asgardeo context and transforms them into a theme object. - * - * The hook ensures the branding API is called only once during the component lifecycle - * unless explicitly refetched using the refetch function. + * React hook for accessing branding preferences from the BrandingProvider context. + * This hook provides access to branding preferences, theme data, and loading states. * - * @param config - Configuration options for the hook + * @deprecated Consider using useBrandingContext directly for better performance. + * This hook is maintained for backward compatibility. + * + * @param config - Configuration options (deprecated, use BrandingProvider props instead) * @returns Object containing branding preference data, theme, loading state, error, and refetch function * * @example @@ -124,136 +114,39 @@ export interface UseBrandingReturn { * ``` * * @example - * With custom configuration: + * For new implementations, use BrandingProvider with useBrandingContext: * ```tsx - * function MyComponent() { - * const { theme, fetchBranding } = useBranding({ - * locale: 'en-US', - * name: 'my-branding', - * type: 'org', - * forceTheme: 'dark', - * autoFetch: false - * }); - * - * useEffect(() => { - * fetchBranding(); - * }, [fetchBranding]); - * - * return
Custom branding component
; - * } - * ``` + * // In your root component + * + * + * * - * @example - * With custom fetcher: - * ```tsx + * // In your component * function MyComponent() { - * const { theme, isLoading, error } = useBranding({ - * fetcher: async (url, config) => { - * // Use your custom HTTP client - * const response = await myHttpClient.request({ - * url, - * method: config.method, - * headers: config.headers, - * ...config - * }); - * return response; - * } - * }); - * - * return
Component with custom fetcher
; + * const { theme, activeTheme, isLoading, error } = useBrandingContext(); + * // ... rest of your component * } * ``` */ export const useBranding = (config: UseBrandingConfig = {}): UseBrandingReturn => { - const {locale, name, type, forceTheme, autoFetch = true, fetcher} = config; - - const {baseUrl, isInitialized} = useAsgardeo(); - - const [brandingPreference, setBrandingPreference] = useState(null); - const [theme, setTheme] = useState(null); - const [activeTheme, setActiveTheme] = useState<'light' | 'dark' | null>(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [hasFetched, setHasFetched] = useState(false); - - const fetchBranding = useCallback(async (): Promise => { - if (!baseUrl) { - setError(new Error('Base URL is not available. Make sure you are using this hook within an AsgardeoProvider.')); - return; - } - - // Prevent multiple calls if already fetching or already fetched (unless explicitly called) - if (isLoading) { - return; - } - - setIsLoading(true); - setError(null); - - try { - const getBrandingConfig: GetBrandingPreferenceConfig = { - baseUrl, - locale, - name, - type, - fetcher, - }; - - const brandingData = await getBrandingPreference(getBrandingConfig); - setBrandingPreference(brandingData); - - // Extract active theme from branding preference - const activeThemeFromBranding = brandingData?.preference?.theme?.activeTheme; - let extractedActiveTheme: 'light' | 'dark' | null = null; - - if (activeThemeFromBranding) { - // Convert to lowercase and map to our expected values - const themeMode = activeThemeFromBranding.toLowerCase(); - if (themeMode === 'light' || themeMode === 'dark') { - extractedActiveTheme = themeMode; - } - } - - setActiveTheme(extractedActiveTheme); - - // Transform branding preference to theme - const transformedTheme = transformBrandingPreferenceToTheme(brandingData, forceTheme); - setTheme(transformedTheme); - setHasFetched(true); - } catch (err) { - const errorMessage = err instanceof Error ? err : new Error('Failed to fetch branding preference'); - setError(errorMessage); - setBrandingPreference(null); - setTheme(null); - setActiveTheme(null); - setHasFetched(true); // Mark as fetched even on error to prevent retries - } finally { - setIsLoading(false); - } - }, [baseUrl, locale, name, type, forceTheme, fetcher, isLoading]); - - // Auto-fetch when dependencies change - but only once - useEffect(() => { - if (autoFetch && isInitialized && baseUrl && !hasFetched) { - fetchBranding(); - } - }, [autoFetch, isInitialized, baseUrl, hasFetched, fetchBranding]); - - // Manual refetch function that bypasses the hasFetched check - const refetch = useCallback(async (): Promise => { - setHasFetched(false); // Reset the flag to allow refetching - await fetchBranding(); - }, [fetchBranding]); - - return { - brandingPreference, - theme, - activeTheme, - isLoading, - error, - fetchBranding, - refetch, - }; + try { + return useBrandingContext(); + } catch (error) { + console.warn( + 'useBranding: BrandingProvider not available. ' + + 'Make sure to wrap your app with BrandingProvider or AsgardeoProvider with branding preferences.', + ); + + return { + brandingPreference: null, + theme: null, + activeTheme: null, + isLoading: false, + error: new Error('BrandingProvider not available'), + fetchBranding: async () => {}, + refetch: async () => {}, + }; + } }; export default useBranding; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 5b0d033c1..0eae6cac9 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -70,6 +70,15 @@ export * from './contexts/Theme/ThemeProvider'; export {default as useTheme} from './contexts/Theme/useTheme'; export * from './contexts/Theme/useTheme'; +export {default as BrandingContext} from './contexts/Branding/BrandingContext'; +export * from './contexts/Branding/BrandingContext'; + +export {default as BrandingProvider} from './contexts/Branding/BrandingProvider'; +export * from './contexts/Branding/BrandingProvider'; + +export {default as useBrandingContext} from './contexts/Branding/useBrandingContext'; +export * from './contexts/Branding/useBrandingContext'; + export {default as useBrowserUrl} from './hooks/useBrowserUrl'; export * from './hooks/useBrowserUrl'; From 0bbc71a4572c9f26660771cd6e566e0adca7abb5 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 17:46:56 +0530 Subject: [PATCH 23/31] feat(react): add branding preference handling to enhance user experience --- .../contexts/Asgardeo/AsgardeoProvider.tsx | 36 ++++++++------ .../nextjs/src/server/AsgardeoProvider.tsx | 29 +++++++++++- .../server/actions/getBrandingPreference.ts | 47 +++++++++++++++++++ 3 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 packages/nextjs/src/server/actions/getBrandingPreference.ts diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index f046908ec..a0ea15131 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -29,6 +29,7 @@ import { UpdateMeProfileConfig, User, UserProfile, + BrandingPreference, } from '@asgardeo/node'; import { I18nProvider, @@ -37,6 +38,7 @@ import { ThemeProvider, AsgardeoProviderProps, OrganizationProvider, + BrandingProvider, } from '@asgardeo/react'; import {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react'; import {useRouter, useSearchParams} from 'next/navigation'; @@ -70,6 +72,7 @@ export type AsgardeoClientProviderProps = Partial Promise; myOrganizations: Organization[]; revalidateMyOrganizations?: (sessionId?: string) => Promise; + brandingPreference?: BrandingPreference | null; }; const AsgardeoClientProvider: FC> = ({ @@ -92,6 +95,7 @@ const AsgardeoClientProvider: FC> myOrganizations, revalidateMyOrganizations, getAllOrganizations, + brandingPreference, }: PropsWithChildren) => { const reRenderCheckRef: RefObject = useRef(false); const router = useRouter(); @@ -307,21 +311,23 @@ const AsgardeoClientProvider: FC> return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 309998ee6..b54633a49 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -19,7 +19,14 @@ 'use server'; import {FC, PropsWithChildren, ReactElement} from 'react'; -import {AllOrganizationsApiResponse, AsgardeoRuntimeError, Organization, User, UserProfile} from '@asgardeo/node'; +import { + BrandingPreference, + AllOrganizationsApiResponse, + AsgardeoRuntimeError, + Organization, + User, + UserProfile, +} from '@asgardeo/node'; import AsgardeoClientProvider from '../client/contexts/Asgardeo/AsgardeoProvider'; import AsgardeoNextClient from '../AsgardeoNextClient'; import signInAction from './actions/signInAction'; @@ -36,6 +43,7 @@ import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction import updateUserProfileAction from './actions/updateUserProfileAction'; import getMyOrganizations from './actions/getMyOrganizations'; import getAllOrganizations from './actions/getAllOrganizations'; +import getBrandingPreference from './actions/getBrandingPreference'; /** * Props interface of {@link AsgardeoServerProvider} @@ -100,6 +108,7 @@ const AsgardeoServerProvider: FC> orgHandle: '', }; let myOrganizations: Organization[] = []; + let brandingPreference: BrandingPreference | null = null; if (_isSignedIn) { const userResponse = await getUserAction(sessionId); @@ -112,6 +121,23 @@ const AsgardeoServerProvider: FC> currentOrganization = currentOrganizationResponse?.data?.organization as Organization; } + // Fetch branding preference if branding is enabled in config + if (config?.preferences?.theme?.inheritFromBranding !== false) { + try { + brandingPreference = await getBrandingPreference( + { + baseUrl: config?.baseUrl, + locale: 'en-US', + name: config.applicationId || config.organizationHandle, + type: config.applicationId ? 'APP' : 'ORG', + }, + sessionId, + ); + } catch (error) { + console.warn('[AsgardeoServerProvider] Failed to fetch branding preference:', error); + } + } + const handleGetAllOrganizations = async ( options?: any, _sessionId?: string, @@ -140,6 +166,7 @@ const AsgardeoServerProvider: FC> isSignedIn={_isSignedIn} myOrganizations={myOrganizations} getAllOrganizations={handleGetAllOrganizations} + brandingPreference={brandingPreference} > {children} diff --git a/packages/nextjs/src/server/actions/getBrandingPreference.ts b/packages/nextjs/src/server/actions/getBrandingPreference.ts new file mode 100644 index 000000000..b2949f199 --- /dev/null +++ b/packages/nextjs/src/server/actions/getBrandingPreference.ts @@ -0,0 +1,47 @@ +/** + * 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 { + AsgardeoAPIError, + GetBrandingPreferenceConfig, + BrandingPreference, + getBrandingPreference as baseGetBrandingPreference, +} from '@asgardeo/node'; + +/** + * Server action to get branding preferences. + */ +const getBrandingPreference = async ( + config: GetBrandingPreferenceConfig, + sessionId?: string | undefined, +): Promise => { + try { + return await baseGetBrandingPreference(config); + } catch (error) { + throw new AsgardeoAPIError( + `Failed to get branding preferences: ${error instanceof Error ? error.message : String(error)}`, + 'getBrandingPreferenceAction-ServerActionError-001', + 'nextjs', + error instanceof AsgardeoAPIError ? error.statusCode : undefined, + ); + } +}; + +export default getBrandingPreference; From 53f8e66f1b45c825cd5bc4a98c42c3fe981ef5c7 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 22:36:12 +0530 Subject: [PATCH 24/31] feat(nextjs): enhance switchOrganization method to include sessionId and return TokenResponse --- .../src/AsgardeoJavaScriptClient.ts | 3 ++- packages/javascript/src/models/client.ts | 3 ++- packages/nextjs/src/AsgardeoNextClient.ts | 9 ++++--- .../contexts/Asgardeo/AsgardeoProvider.tsx | 23 ++-------------- .../nextjs/src/server/AsgardeoProvider.tsx | 17 +++++++++++- ...izationAction.ts => switchOrganization.ts} | 26 +++++++++---------- packages/react/src/AsgardeoReactClient.ts | 7 ++--- 7 files changed, 44 insertions(+), 44 deletions(-) rename packages/nextjs/src/server/actions/{switchOrganizationAction.ts => switchOrganization.ts} (53%) diff --git a/packages/javascript/src/AsgardeoJavaScriptClient.ts b/packages/javascript/src/AsgardeoJavaScriptClient.ts index 3085cadb7..1e58e4fa0 100644 --- a/packages/javascript/src/AsgardeoJavaScriptClient.ts +++ b/packages/javascript/src/AsgardeoJavaScriptClient.ts @@ -21,6 +21,7 @@ import {AsgardeoClient, SignInOptions, SignOutOptions, SignUpOptions} from './mo import {Config} from './models/config'; import {EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse} from './models/embedded-flow'; import {EmbeddedSignInFlowHandleRequestPayload} from './models/embedded-signin-flow'; +import {TokenResponse} from './models/token'; import {Organization} from './models/organization'; import {User, UserProfile} from './models/user'; @@ -31,7 +32,7 @@ import {User, UserProfile} from './models/user'; * @typeParam T - Configuration type that extends Config. */ abstract class AsgardeoJavaScriptClient implements AsgardeoClient { - abstract switchOrganization(organization: Organization): Promise; + abstract switchOrganization(organization: Organization, sessionId?: string): Promise; abstract initialize(config: T): Promise; diff --git a/packages/javascript/src/models/client.ts b/packages/javascript/src/models/client.ts index 82dfb128e..722714fa2 100644 --- a/packages/javascript/src/models/client.ts +++ b/packages/javascript/src/models/client.ts @@ -25,6 +25,7 @@ import { import {EmbeddedSignInFlowHandleRequestPayload} from './embedded-signin-flow'; import {Organization} from './organization'; import {User, UserProfile} from './user'; +import {TokenResponse} from './token'; export type SignInOptions = Record; export type SignOutOptions = Record; @@ -62,7 +63,7 @@ export interface AsgardeoClient { * @param organization - The organization to switch to. * @returns A promise that resolves when the switch is complete. */ - switchOrganization(organization: Organization): Promise; + switchOrganization(organization: Organization, sessionId?: string): Promise ; getConfiguration(): T; diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 84602143b..a109d2dd4 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -50,8 +50,8 @@ import { getAllOrganizations, AllOrganizationsApiResponse, extractUserClaimsFromIdToken, + TokenResponse } from '@asgardeo/node'; -import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; import getSessionId from './server/actions/getSessionId'; import decorateConfigWithNextEnv from './utils/decorateConfigWithNextEnv'; @@ -329,7 +329,7 @@ class AsgardeoNextClient exte }; } - override async switchOrganization(organization: Organization, userId?: string): Promise { + override async switchOrganization(organization: Organization, userId?: string): Promise { try { const configData = await this.asgardeo.getConfigData(); const scopes = configData?.scopes; @@ -347,6 +347,7 @@ class AsgardeoNextClient exte attachToken: false, data: { client_id: '{{clientId}}', + client_secret: '{{clientSecret}}', grant_type: 'organization_switch', scope: '{{scopes}}', switching_organization: organization.id, @@ -357,10 +358,10 @@ class AsgardeoNextClient exte signInRequired: true, }; - await this.asgardeo.exchangeToken(exchangeConfig, userId); + return await this.asgardeo.exchangeToken(exchangeConfig, userId); } catch (error) { throw new AsgardeoRuntimeError( - `Failed to switch organization: ${error instanceof Error ? error.message : String(error)}`, + `Failed to switch organization: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`, 'AsgardeoReactClient-RuntimeError-003', 'nextjs', 'An error occurred while switching to the specified organization. Please try again.', diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index a0ea15131..fbdc54baf 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -43,8 +43,6 @@ import { import {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react'; import {useRouter, useSearchParams} from 'next/navigation'; import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext'; -import getSessionId from '../../../server/actions/getSessionId'; -import switchOrganizationAction from '../../../server/actions/switchOrganizationAction'; /** * Props interface of {@link AsgardeoClientProvider} @@ -73,6 +71,7 @@ export type AsgardeoClientProviderProps = Partial Promise; brandingPreference?: BrandingPreference | null; + switchOrganization: (organization: Organization, sessionId?: string) => Promise; }; const AsgardeoClientProvider: FC> = ({ @@ -95,6 +94,7 @@ const AsgardeoClientProvider: FC> myOrganizations, revalidateMyOrganizations, getAllOrganizations, + switchOrganization, brandingPreference, }: PropsWithChildren) => { const reRenderCheckRef: RefObject = useRef(false); @@ -263,25 +263,6 @@ 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, diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index b54633a49..c5ea70311 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -44,6 +44,7 @@ import updateUserProfileAction from './actions/updateUserProfileAction'; import getMyOrganizations from './actions/getMyOrganizations'; import getAllOrganizations from './actions/getAllOrganizations'; import getBrandingPreference from './actions/getBrandingPreference'; +import switchOrganization from './actions/switchOrganization'; /** * Props interface of {@link AsgardeoServerProvider} @@ -126,7 +127,7 @@ const AsgardeoServerProvider: FC> try { brandingPreference = await getBrandingPreference( { - baseUrl: config?.baseUrl, + baseUrl: config?.baseUrl as string, locale: 'en-US', name: config.applicationId || config.organizationHandle, type: config.applicationId ? 'APP' : 'ORG', @@ -146,6 +147,19 @@ const AsgardeoServerProvider: FC> return await getAllOrganizations(options, sessionId); }; + const handleSwitchOrganization = async (organization: Organization, _sessionId?: string): Promise => { + 'use server'; + await switchOrganization(organization, sessionId); + + // After switching organization, we need to refresh the page to get updated session data + // This is because server components don't maintain state between function calls + const { revalidatePath } = await import('next/cache'); + const { redirect } = await import('next/navigation'); + + // Revalidate the current path to refresh the component with new data + revalidatePath('/'); + }; + return ( > isSignedIn={_isSignedIn} myOrganizations={myOrganizations} getAllOrganizations={handleGetAllOrganizations} + switchOrganization={handleSwitchOrganization} brandingPreference={brandingPreference} > {children} diff --git a/packages/nextjs/src/server/actions/switchOrganizationAction.ts b/packages/nextjs/src/server/actions/switchOrganization.ts similarity index 53% rename from packages/nextjs/src/server/actions/switchOrganizationAction.ts rename to packages/nextjs/src/server/actions/switchOrganization.ts index 9b049bacd..4441d58de 100644 --- a/packages/nextjs/src/server/actions/switchOrganizationAction.ts +++ b/packages/nextjs/src/server/actions/switchOrganization.ts @@ -18,26 +18,26 @@ 'use server'; -import {Organization, OrganizationDetails} from '@asgardeo/node'; +import {Organization, AsgardeoAPIError, AsgardeoRuntimeError, TokenResponse} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; /** - * Server action to create an organization. + * Server action to switch organization. */ -const switchOrganizationAction = async (organization: Organization, sessionId: string) => { +const switchOrganization = async (organization: Organization, sessionId: string): Promise => { try { const client = AsgardeoNextClient.getInstance(); - await client.switchOrganization(organization, sessionId); - return {success: true, error: null}; + return await client.switchOrganization(organization, sessionId); } catch (error) { - return { - success: false, - data: { - user: {}, - }, - error: 'Failed to switch to organization', - }; + throw new AsgardeoAPIError( + `Failed to switch the organizations: ${ + error instanceof AsgardeoRuntimeError ? error.message : error instanceof Error ? error.message : String(error) + }`, + 'switchOrganization-ServerActionError-001', + 'nextjs', + error instanceof AsgardeoAPIError ? error.statusCode : undefined, + ); } }; -export default switchOrganizationAction; +export default switchOrganization; diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index d41f0fc2f..60e38fe14 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -38,6 +38,7 @@ import { deriveOrganizationHandleFromBaseUrl, AllOrganizationsApiResponse, extractUserClaimsFromIdToken, + TokenResponse, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import getMeOrganizations from './api/getMeOrganizations'; @@ -180,7 +181,7 @@ class AsgardeoReactClient e }; } - override async switchOrganization(organization: Organization): Promise { + override async switchOrganization(organization: Organization, sessionId?: string): Promise { try { const configData = await this.asgardeo.getConfigData(); const scopes = configData?.scopes; @@ -208,11 +209,11 @@ class AsgardeoReactClient e signInRequired: true, }; - await this.asgardeo.exchangeToken( + return await this.asgardeo.exchangeToken( exchangeConfig, (user: User) => {}, () => null, - ); + ) as TokenResponse | Response; } catch (error) { throw new AsgardeoRuntimeError( `Failed to switch organization: ${error.message || error}`, From 8f2a609ac559b3bed1a00bfff63dc74113bc95b1 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 22:41:58 +0530 Subject: [PATCH 25/31] chore(nextjs): clean up import statements in AsgardeoServerProvider component --- packages/nextjs/src/server/AsgardeoProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index c5ea70311..08391aacb 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -153,8 +153,8 @@ const AsgardeoServerProvider: FC> // After switching organization, we need to refresh the page to get updated session data // This is because server components don't maintain state between function calls - const { revalidatePath } = await import('next/cache'); - const { redirect } = await import('next/navigation'); + const {revalidatePath} = await import('next/cache'); + const {redirect} = await import('next/navigation'); // Revalidate the current path to refresh the component with new data revalidatePath('/'); From e14084848ee5634177b285751657cbb6e269c775 Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 22:50:26 +0530 Subject: [PATCH 26/31] fix(react): update background property to backgroundColor and correct SVG stroke attributes --- .../OrganizationSwitcher/BaseOrganizationSwitcher.tsx | 2 +- .../presentation/SignIn/options/MultiOptionButton.tsx | 2 +- .../presentation/UserDropdown/BaseUserDropdown.tsx | 4 ++-- .../react/src/components/primitives/Icons/BuildingAlt.tsx | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx index 4b21ce51d..fecc96d22 100644 --- a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx +++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx @@ -93,7 +93,7 @@ const useStyles = () => { color: theme.vars.colors.text.primary, textDecoration: 'none', border: 'none', - background: 'none', + backgroundColor: 'none', cursor: 'pointer', fontSize: '0.875rem', textAlign: 'left', diff --git a/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx b/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx index dfdac78b5..c603f7dd8 100644 --- a/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx +++ b/packages/react/src/components/presentation/SignIn/options/MultiOptionButton.tsx @@ -95,7 +95,7 @@ const MultiOptionButton: FC = ({ case ApplicationNativeAuthenticationConstants.SupportedAuthenticators.Passkey: return ( - + {' '} diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx index 83a3a7fa2..890379a16 100644 --- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.tsx @@ -51,7 +51,7 @@ const useStyles = () => { gap: theme.vars.spacing.unit, padding: `calc(${theme.vars.spacing.unit} * 0.5)`, border: 'none', - background: 'none', + backgroundColor: 'none', cursor: 'pointer', borderRadius: theme.vars.borderRadius.medium, '&:hover': { @@ -91,7 +91,7 @@ const useStyles = () => { color: theme.vars.colors.text.primary, textDecoration: 'none', border: 'none', - background: 'none', + backgroundColor: 'none', cursor: 'pointer', fontSize: '0.875rem', textAlign: 'left', diff --git a/packages/react/src/components/primitives/Icons/BuildingAlt.tsx b/packages/react/src/components/primitives/Icons/BuildingAlt.tsx index ba39223d0..81eaec8d1 100644 --- a/packages/react/src/components/primitives/Icons/BuildingAlt.tsx +++ b/packages/react/src/components/primitives/Icons/BuildingAlt.tsx @@ -47,9 +47,9 @@ const BuildingAlt: FC = ({color = 'currentColor', height = 24, fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" > From dc545144cc04d4fd0c3538e8ecd0cdd7e882641c Mon Sep 17 00:00:00 2001 From: Brion Date: Thu, 3 Jul 2025 23:58:29 +0530 Subject: [PATCH 27/31] feat(nextjs): add getDecodedIdToken method to AsgardeoNextClient and utilize it in AsgardeoServerProvider for organization login handling --- packages/nextjs/src/AsgardeoNextClient.ts | 8 ++++++++ packages/nextjs/src/server/AsgardeoProvider.tsx | 12 +++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index a109d2dd4..2691a4916 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -381,6 +381,14 @@ class AsgardeoNextClient exte return this.asgardeo.getAccessToken(sessionId as string); } + /** + * Get the decoded ID token for a session + */ + async getDecodedIdToken(sessionId?: string): Promise { + await this.ensureInitialized(); + return this.asgardeo.getDecodedIdToken(sessionId as string); + } + override getConfiguration(): T { return this.asgardeo.getConfigData() as unknown as T; } diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 08391aacb..cf9eacc8d 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -26,6 +26,7 @@ import { Organization, User, UserProfile, + IdToken, } from '@asgardeo/node'; import AsgardeoClientProvider from '../client/contexts/Asgardeo/AsgardeoProvider'; import AsgardeoNextClient from '../AsgardeoNextClient'; @@ -112,6 +113,16 @@ const AsgardeoServerProvider: FC> let brandingPreference: BrandingPreference | null = null; if (_isSignedIn) { + // Check if there's a `user_org` claim in the ID token to determine if this is an organization login + const idToken = await asgardeoClient.getDecodedIdToken(sessionId); + let updatedBaseUrl = config?.baseUrl; + + if (idToken?.['user_org']) { + // Treat this login as an organization login and modify the base URL + updatedBaseUrl = `${config?.baseUrl}/o`; + config = { ...config, baseUrl: updatedBaseUrl }; + } + const userResponse = await getUserAction(sessionId); const userProfileResponse = await getUserProfileAction(sessionId); const currentOrganizationResponse = await getCurrentOrganizationAction(sessionId); @@ -154,7 +165,6 @@ const AsgardeoServerProvider: FC> // After switching organization, we need to refresh the page to get updated session data // This is because server components don't maintain state between function calls const {revalidatePath} = await import('next/cache'); - const {redirect} = await import('next/navigation'); // Revalidate the current path to refresh the component with new data revalidatePath('/'); From 1196ff6a1e53c695e832ab35ee0d217800232b82 Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 4 Jul 2025 00:25:21 +0530 Subject: [PATCH 28/31] fix(nextjs): standardize formatting in AsgardeoNextClient and AsgardeoProvider components --- packages/nextjs/src/AsgardeoNextClient.ts | 2 +- packages/nextjs/src/server/AsgardeoProvider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 2691a4916..8e302b1a9 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -50,7 +50,7 @@ import { getAllOrganizations, AllOrganizationsApiResponse, extractUserClaimsFromIdToken, - TokenResponse + TokenResponse, } from '@asgardeo/node'; import {AsgardeoNextConfig} from './models/config'; import getSessionId from './server/actions/getSessionId'; diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index cf9eacc8d..d5671c4bc 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -120,7 +120,7 @@ const AsgardeoServerProvider: FC> if (idToken?.['user_org']) { // Treat this login as an organization login and modify the base URL updatedBaseUrl = `${config?.baseUrl}/o`; - config = { ...config, baseUrl: updatedBaseUrl }; + config = {...config, baseUrl: updatedBaseUrl}; } const userResponse = await getUserAction(sessionId); From fc80f77072856852bb934c420c311a1c6df3ee79 Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 4 Jul 2025 01:21:00 +0530 Subject: [PATCH 29/31] feat: add image configuration support in theme and update related components --- packages/javascript/src/theme/createTheme.ts | 45 +- packages/javascript/src/theme/types.ts | 53 ++ ...transformBrandingPreferenceToTheme.test.ts | 519 ++++-------------- .../transformBrandingPreferenceToTheme.ts | 28 + .../presentation/SignIn/BaseSignIn.tsx | 57 ++ .../presentation/SignUp/BaseSignUp.tsx | 35 ++ .../src/components/primitives/Logo/Logo.tsx | 104 ++++ .../src/contexts/Theme/ThemeProvider.tsx | 5 + packages/react/src/index.ts | 3 + .../teamspace-react/src/pages/SignInPage.tsx | 11 - .../teamspace-react/src/pages/SignUpPage.tsx | 11 - 11 files changed, 426 insertions(+), 445 deletions(-) create mode 100644 packages/react/src/components/primitives/Logo/Logo.tsx diff --git a/packages/javascript/src/theme/createTheme.ts b/packages/javascript/src/theme/createTheme.ts index 616fbc22c..59bc9393f 100644 --- a/packages/javascript/src/theme/createTheme.ts +++ b/packages/javascript/src/theme/createTheme.ts @@ -120,6 +120,10 @@ const lightTheme: ThemeConfig = { relaxed: 1.6, }, }, + images: { + favicon: {}, + logo: {}, + }, }; const darkTheme: ThemeConfig = { @@ -206,6 +210,10 @@ const darkTheme: ThemeConfig = { relaxed: 1.6, }, }, + images: { + favicon: {}, + logo: {}, + }, }; const toCssVariables = (theme: ThemeConfig): Record => { @@ -391,13 +399,29 @@ const toCssVariables = (theme: ThemeConfig): Record => { cssVars[`--${prefix}-typography-lineHeight-relaxed`] = theme.typography.lineHeights.relaxed.toString(); } + // Images + if (theme.images) { + Object.keys(theme.images).forEach(imageKey => { + const imageConfig = theme.images![imageKey]; + if (imageConfig?.url) { + cssVars[`--${prefix}-image-${imageKey}-url`] = imageConfig.url; + } + if (imageConfig?.title) { + cssVars[`--${prefix}-image-${imageKey}-title`] = imageConfig.title; + } + if (imageConfig?.alt) { + cssVars[`--${prefix}-image-${imageKey}-alt`] = imageConfig.alt; + } + }); + } + return cssVars; }; const toThemeVars = (theme: ThemeConfig): ThemeVars => { const prefix = theme.cssVarPrefix || VendorConstants.VENDOR_PREFIX; - return { + const themeVars: ThemeVars = { colors: { action: { active: `var(--${prefix}-color-action-active)`, @@ -482,6 +506,21 @@ const toThemeVars = (theme: ThemeConfig): ThemeVars => { }, }, }; + + // Add images if they exist + if (theme.images) { + themeVars.images = {}; + Object.keys(theme.images).forEach(imageKey => { + const imageConfig = theme.images![imageKey]; + themeVars.images![imageKey] = { + url: imageConfig?.url ? `var(--${prefix}-image-${imageKey}-url)` : undefined, + title: imageConfig?.title ? `var(--${prefix}-image-${imageKey}-title)` : undefined, + alt: imageConfig?.alt ? `var(--${prefix}-image-${imageKey}-alt)` : undefined, + }; + }); + } + + return themeVars; }; const createTheme = (config: RecursivePartial = {}, isDark = false): Theme => { @@ -530,6 +569,10 @@ const createTheme = (config: RecursivePartial = {}, isDark = false) ...(config.typography?.lineHeights || {}), }, }, + images: { + ...baseTheme.images, + ...config.images, + }, } as ThemeConfig; return { diff --git a/packages/javascript/src/theme/types.ts b/packages/javascript/src/theme/types.ts index 0bc6e0142..9fee3ad6a 100644 --- a/packages/javascript/src/theme/types.ts +++ b/packages/javascript/src/theme/types.ts @@ -126,6 +126,10 @@ export interface ThemeConfig { relaxed: number; }; }; + /** + * Image assets configuration + */ + images?: ThemeImages; /** * The prefix used for CSS variables. * @default 'asgardeo' (from VendorConstants.VENDOR_PREFIX) @@ -217,6 +221,25 @@ export interface ThemeVars { relaxed: string; }; }; + images?: { + favicon?: { + url?: string; + title?: string; + alt?: string; + }; + logo?: { + url?: string; + title?: string; + alt?: string; + }; + [key: string]: + | { + url?: string; + title?: string; + alt?: string; + } + | undefined; + }; } export interface Theme extends ThemeConfig { @@ -238,3 +261,33 @@ export interface ThemeDetection { */ lightClass?: string; } + +export interface ThemeImage { + /** + * The URL of the image + */ + url?: string; + /** + * The title/alt text for the image + */ + title?: string; + /** + * Alternative text for accessibility + */ + alt?: string; +} + +export interface ThemeImages { + /** + * Favicon configuration + */ + favicon?: ThemeImage; + /** + * Logo configuration + */ + logo?: ThemeImage; + /** + * Allow for additional custom images + */ + [key: string]: ThemeImage | undefined; +} diff --git a/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts b/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts index 8508517a5..7973187c5 100644 --- a/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts +++ b/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts @@ -1,444 +1,119 @@ /** - * 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. + * Test example for transformBrandingPreferenceToTheme with images */ - -import {describe, it, expect} from 'vitest'; import {transformBrandingPreferenceToTheme} from '../transformBrandingPreferenceToTheme'; import {BrandingPreference} from '../../models/branding-preference'; -describe('transformBrandingPreferenceToTheme', () => { - const mockBrandingPreference: BrandingPreference = { - locale: 'en-US', - name: 'dxlab', - preference: { - configs: { - isBrandingEnabled: true, - removeDefaultBranding: false, - }, - layout: { - activeLayout: 'centered', - }, - organizationDetails: { - displayName: '', - supportEmail: '', - }, - theme: { - activeTheme: 'LIGHT', - LIGHT: { - buttons: { - externalConnection: { - base: { - background: { - backgroundColor: '#FFFFFF', - }, - border: { - borderRadius: '8px', - }, - font: { - color: '#000000de', - }, - }, - }, - primary: { - base: { - border: { - borderRadius: '8px', - }, - font: { - color: '#ffffffe6', - }, - }, - }, - secondary: { - base: { - border: { - borderRadius: '8px', - }, - font: { - color: '#000000de', - }, - }, - }, +// Example branding preference with images +const mockBrandingPreference: BrandingPreference = { + type: 'ORG', + name: 'dxlab', + locale: 'en-US', + preference: { + theme: { + activeTheme: 'LIGHT', + LIGHT: { + images: { + favicon: { + imgURL: 'https://example.com/favicon.ico', + title: 'My App Favicon', + altText: 'Application Icon', }, - colors: { - alerts: { - error: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#ffd8d8', - }, - info: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#eff7fd', - }, - neutral: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#f8f8f9', - }, - warning: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#fff6e7', - }, - }, - background: { - body: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#fbfbfb', - }, - surface: { - contrastText: '', - dark: '#F6F4F2', - inverted: '#212A32', - light: '#f9fafb', - main: '#ffffff', - }, - }, - illustrations: { - accent1: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#3865B5', - }, - accent2: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#19BECE', - }, - accent3: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#FFFFFF', - }, - primary: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#FF7300', - }, - secondary: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#E0E1E2', - }, - }, - outlined: { - default: '#dadce0', - }, - primary: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#2563eb', - }, - secondary: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#E0E1E2', - }, - text: { - primary: '#000000de', - secondary: '#00000066', - }, + logo: { + imgURL: 'https://example.com/logo.png', + title: 'Company Logo', + altText: 'Company Brand Logo', }, - footer: { - border: { - borderColor: '', - }, - font: { - color: '', - }, - }, - images: { - favicon: {}, - logo: { - imgURL: - 'https://cdn.statically.io/gh/brionmario/javascript/refs/heads/next/samples/teamspace-react/public/teamspace-logo.png', - }, - myAccountLogo: { - title: 'Account', - }, + }, + colors: { + primary: { + main: '#FF7300', + contrastText: '#ffffff', }, - inputs: { - base: { - background: { - backgroundColor: '#FFFFFF', - }, - border: { - borderColor: '', - borderRadius: '8px', - }, - font: { - color: '', - }, - labels: { - font: { - color: '', - }, - }, - }, + secondary: { + main: '#E0E1E2', + contrastText: '#000000', }, - loginBox: { - background: { - backgroundColor: '', - }, - border: { - borderColor: '', - borderRadius: '12px', - borderWidth: '1px', + background: { + surface: { + main: '#ffffff', }, - font: { - color: '', + body: { + main: '#fbfbfb', }, }, - loginPage: { - background: { - backgroundColor: '', - }, - font: { - color: '', - }, + text: { + primary: '#000000de', + secondary: '#00000066', }, - typography: { - font: { - fontFamily: 'Gilmer', - importURL: '', - }, - heading: { - font: { - color: '', - }, - }, + }, + }, + DARK: { + images: { + favicon: { + imgURL: 'https://example.com/favicon-dark.ico', + title: 'My App Favicon Dark', + altText: 'Application Icon Dark', + }, + logo: { + imgURL: 'https://example.com/logo-dark.png', + title: 'Company Logo Dark', + altText: 'Company Brand Logo Dark', }, }, - DARK: { - buttons: { - externalConnection: { - base: { - background: { - backgroundColor: '#24292e', - }, - border: { - borderRadius: '22px', - }, - font: { - color: '#ffffff', - }, - }, - }, - primary: { - base: { - border: { - borderRadius: '22px', - }, - font: { - color: '#ffffff', - }, - }, - }, - secondary: { - base: { - border: { - borderRadius: '22px', - }, - font: { - color: '#000000', - }, - }, - }, + colors: { + primary: { + main: '#FF7300', + contrastText: '#ffffff', }, - colors: { - alerts: { - error: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#ff000054', - }, - info: { - contrastText: '', - dark: '#01579b', - inverted: '', - light: '', - main: '#0288d1', - }, - }, - background: { - body: { - contrastText: '', - dark: '', - inverted: '', - light: '', - main: '#121212', - }, - surface: { - contrastText: '', - dark: '#1e1e1e', - inverted: '#ffffff', - light: '#2c2c2c', - main: '#1a1a1a', - }, + background: { + surface: { + main: '#242627', }, - primary: { - contrastText: '#ffffff', - dark: '#1976d2', - inverted: '', - light: '#42a5f5', - main: '#2196f3', - }, - secondary: { - contrastText: '#ffffff', - dark: '#388e3c', - inverted: '', - light: '#66bb6a', - main: '#4caf50', - }, - text: { - primary: '#ffffff', - secondary: '#b3b3b3', + body: { + main: '#17191a', }, }, - }, - }, - }, - }; - - it('should transform branding preference to theme using active theme', () => { - const theme = transformBrandingPreferenceToTheme(mockBrandingPreference); - - expect(theme).toBeDefined(); - expect(theme.colors).toBeDefined(); - expect(theme.colors.primary.main).toBe('#2563eb'); - expect(theme.colors.secondary.main).toBe('#E0E1E2'); - expect(theme.colors.background.surface).toBe('#ffffff'); - expect(theme.colors.background.body.main).toBe('#fbfbfb'); - expect(theme.colors.text.primary).toBe('#000000de'); - expect(theme.colors.text.secondary).toBe('#00000066'); - expect(theme.colors.border).toBe('#dadce0'); - expect(theme.borderRadius.small).toBe('8px'); - expect(theme.cssVariables).toBeDefined(); - }); - - it('should force light theme when specified', () => { - const theme = transformBrandingPreferenceToTheme(mockBrandingPreference, 'light'); - - expect(theme.colors.primary.main).toBe('#2563eb'); - expect(theme.colors.background.surface).toBe('#ffffff'); - expect(theme.colors.text.primary).toBe('#000000de'); - }); - - it('should force dark theme when specified', () => { - const theme = transformBrandingPreferenceToTheme(mockBrandingPreference, 'dark'); - - expect(theme.colors.primary.main).toBe('#2196f3'); - expect(theme.colors.primary.contrastText).toBe('#ffffff'); - expect(theme.colors.background.surface).toBe('#1a1a1a'); - expect(theme.colors.background.body.main).toBe('#121212'); - expect(theme.colors.text.primary).toBe('#ffffff'); - expect(theme.colors.text.secondary).toBe('#b3b3b3'); - }); - - it('should handle empty branding preference', () => { - const emptyBrandingPreference: BrandingPreference = {}; - const theme = transformBrandingPreferenceToTheme(emptyBrandingPreference); - - expect(theme).toBeDefined(); - expect(theme.colors).toBeDefined(); - expect(theme.cssVariables).toBeDefined(); - }); - - it('should handle branding preference without theme config', () => { - const brandingPreferenceWithoutTheme: BrandingPreference = { - preference: { - configs: { - isBrandingEnabled: true, - }, - }, - }; - const theme = transformBrandingPreferenceToTheme(brandingPreferenceWithoutTheme); - - expect(theme).toBeDefined(); - expect(theme.colors).toBeDefined(); - expect(theme.cssVariables).toBeDefined(); - }); - - it('should handle branding preference with missing theme variant', () => { - const brandingPreferenceWithMissingVariant: BrandingPreference = { - preference: { - theme: { - activeTheme: 'NONEXISTENT', - }, - }, - }; - const theme = transformBrandingPreferenceToTheme(brandingPreferenceWithMissingVariant); - - expect(theme).toBeDefined(); - expect(theme.colors).toBeDefined(); - expect(theme.cssVariables).toBeDefined(); - }); - - it('should use default values for missing color properties', () => { - const minimalBrandingPreference: BrandingPreference = { - preference: { - theme: { - activeTheme: 'LIGHT', - LIGHT: { - colors: { - primary: { - main: '#ff0000', - }, - }, + text: { + primary: '#EBEBEF', + secondary: '#B9B9C6', }, }, }, - }; - const theme = transformBrandingPreferenceToTheme(minimalBrandingPreference); - - expect(theme.colors.primary.main).toBe('#ff0000'); - expect(theme.colors.primary.contrastText).toBe('#ffffff'); // Default value - expect(theme.colors.secondary.main).toBe('#424242'); // Default value - expect(theme.colors.error.main).toBe('#d32f2f'); // Default value - expect(theme.colors.success.main).toBe('#4caf50'); // Default value - expect(theme.colors.warning.main).toBe('#ff9800'); // Default value - }); -}); + }, + }, +}; + +// Transform the branding preference to theme +const lightTheme = transformBrandingPreferenceToTheme(mockBrandingPreference, 'light'); +const darkTheme = transformBrandingPreferenceToTheme(mockBrandingPreference, 'dark'); + +console.log('=== LIGHT THEME ==='); +console.log('Images:', lightTheme.images); +console.log( + 'CSS Variables (images only):', + Object.keys(lightTheme.cssVariables) + .filter(key => key.includes('image')) + .reduce((obj, key) => { + obj[key] = lightTheme.cssVariables[key]; + return obj; + }, {} as Record), +); + +console.log('\n=== DARK THEME ==='); +console.log('Images:', darkTheme.images); +console.log( + 'CSS Variables (images only):', + Object.keys(darkTheme.cssVariables) + .filter(key => key.includes('image')) + .reduce((obj, key) => { + obj[key] = darkTheme.cssVariables[key]; + return obj; + }, {} as Record), +); + +console.log('\n=== THEME VARIABLES ==='); +console.log('Light theme vars.images:', lightTheme.vars.images); +console.log('Dark theme vars.images:', darkTheme.vars.images); + +export {lightTheme, darkTheme}; diff --git a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts index 16142a312..4f04984f9 100644 --- a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts +++ b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts @@ -41,6 +41,7 @@ const transformThemeVariant = (themeVariant: ThemeVariant, isDark = false): Part const colors = themeVariant.colors; const buttons = themeVariant.buttons; const inputs = themeVariant.inputs; + const images = themeVariant.images; return { colors: { @@ -96,6 +97,23 @@ const transformThemeVariant = (themeVariant: ThemeVariant, isDark = false): Part medium: buttons?.secondary?.base?.border?.borderRadius, large: buttons?.externalConnection?.base?.border?.borderRadius, }, + // Extract and transform images + images: { + favicon: images?.favicon + ? { + url: images.favicon.imgURL, + title: images.favicon.title, + alt: images.favicon.altText, + } + : undefined, + logo: images?.logo + ? { + url: images.logo.imgURL, + title: images.logo.title, + alt: images.logo.altText, + } + : undefined, + }, }; }; @@ -107,11 +125,21 @@ const transformThemeVariant = (themeVariant: ThemeVariant, isDark = false): Part * if not provided, will use the activeTheme from branding preference * @returns Theme object that can be used with the theme system * + * The function extracts the following from branding preference: + * - Colors (primary, secondary, background, text, alerts, etc.) + * - Border radius from buttons and inputs + * - Images (logo and favicon with their URLs, titles, and alt text) + * - Typography settings + * * @example * ```typescript * const brandingPreference = await getBrandingPreference({ baseUrl: "..." }); * const theme = transformBrandingPreferenceToTheme(brandingPreference); * + * // Access image URLs via CSS variables + * // Logo: var(--wso2-image-logo-url) + * // Favicon: var(--wso2-image-favicon-url) + * * // Force light theme regardless of branding preference activeTheme * const lightTheme = transformBrandingPreferenceToTheme(brandingPreference, 'light'); * ``` diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx index 95369ef14..0e6335b6b 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -40,6 +40,7 @@ import useTheme from '../../../contexts/Theme/useTheme'; import Alert from '../../primitives/Alert/Alert'; import Card, {CardProps} from '../../primitives/Card/Card'; import Divider from '../../primitives/Divider/Divider'; +import Logo from '../../primitives/Logo/Logo'; import Spinner from '../../primitives/Spinner/Spinner'; import Typography from '../../primitives/Typography/Typography'; @@ -1093,6 +1094,18 @@ const BaseSignInContent: FC = ({ if (!isInitialized && isLoading) { return ( + +
+ +
+
= ({ return ( +
+ +
{flowTitle || t('signin.title')} {flowSubtitle && ( @@ -1250,6 +1273,18 @@ const BaseSignInContent: FC = ({ if (!currentAuthenticator) { return ( + +
+ +
+
{error && ( @@ -1272,6 +1307,18 @@ const BaseSignInContent: FC = ({ // Show loading state while passkey authentication is in progress return ( + +
+ +
+
@@ -1293,6 +1340,16 @@ const BaseSignInContent: FC = ({ return ( +
+ +
{flowTitle || t('signin.title')} {flowSubtitle || t('signin.subtitle')} diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 07525000b..6b6d91199 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -35,6 +35,7 @@ import useTranslation from '../../../hooks/useTranslation'; import useTheme from '../../../contexts/Theme/useTheme'; import Alert from '../../primitives/Alert/Alert'; import Card, {CardProps} from '../../primitives/Card/Card'; +import Logo from '../../primitives/Logo/Logo'; import Spinner from '../../primitives/Spinner/Spinner'; import Typography from '../../primitives/Typography/Typography'; @@ -635,6 +636,18 @@ const BaseSignUpContent: FC = ({ if (!isFlowInitialized && isLoading) { return ( + +
+ +
+
@@ -647,6 +660,18 @@ const BaseSignUpContent: FC = ({ if (!currentFlow) { return ( + +
+ +
+
{t('errors.title') || 'Error'} @@ -660,6 +685,16 @@ const BaseSignUpContent: FC = ({ return ( +
+ +
{flowMessages && flowMessages.length > 0 && (
{flowMessages.map((message: any, index: number) => ( diff --git a/packages/react/src/components/primitives/Logo/Logo.tsx b/packages/react/src/components/primitives/Logo/Logo.tsx new file mode 100644 index 000000000..1b7304b57 --- /dev/null +++ b/packages/react/src/components/primitives/Logo/Logo.tsx @@ -0,0 +1,104 @@ +/** + * 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. + */ + +import {FC} from 'react'; +import {clsx} from 'clsx'; +import {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import useTheme from '../../../contexts/Theme/useTheme'; + +/** + * Props for the Logo component. + */ +export interface LogoProps { + /** + * Custom CSS class name for the logo. + */ + className?: string; + /** + * Custom logo URL to override theme logo. + */ + src?: string; + /** + * Custom alt text for the logo. + */ + alt?: string; + /** + * Custom title for the logo. + */ + title?: string; + /** + * Size of the logo. + */ + size?: 'small' | 'medium' | 'large'; + /** + * Custom style object. + */ + style?: React.CSSProperties; +} + +/** + * Logo component that displays the brand logo from theme or custom source. + * + * @param props - The props for the Logo component. + * @returns The rendered Logo component. + */ +const Logo: FC = ({className, src, alt, title, size = 'medium', style}) => { + const {theme} = useTheme(); + + // Get logo configuration from theme - use actual values, not CSS variables + // Access the actual theme config values, not the CSS variable references from .vars + const logoConfig = theme.images?.logo; + + const logoSrc = src || logoConfig?.url; + + const logoAlt = alt || logoConfig?.alt || 'Logo'; + + const logoTitle = title || logoConfig?.title; + + const logoClasses = clsx(withVendorCSSClassPrefix('logo'), withVendorCSSClassPrefix(`logo--${size}`), className); + + const sizeStyles: Record = { + small: { + height: '32px', + maxWidth: '120px', + }, + medium: { + height: '48px', + maxWidth: '180px', + }, + large: { + height: '64px', + maxWidth: '240px', + }, + }; + + const defaultStyles: React.CSSProperties = { + width: 'auto', + objectFit: 'contain', + ...sizeStyles[size], + ...style, + }; + + if (!logoSrc) { + return null; + } + + return {logoAlt}; +}; + +export default Logo; diff --git a/packages/react/src/contexts/Theme/ThemeProvider.tsx b/packages/react/src/contexts/Theme/ThemeProvider.tsx index cc23970a0..204801a8b 100644 --- a/packages/react/src/contexts/Theme/ThemeProvider.tsx +++ b/packages/react/src/contexts/Theme/ThemeProvider.tsx @@ -174,6 +174,7 @@ const ThemeProvider: FC> = ({ borderRadius: brandingTheme.borderRadius, shadows: brandingTheme.shadows, spacing: brandingTheme.spacing, + images: brandingTheme.images, }; // Merge branding theme with user-provided theme config @@ -197,6 +198,10 @@ const ThemeProvider: FC> = ({ ...brandingThemeConfig.spacing, ...themeConfig?.spacing, }, + images: { + ...brandingThemeConfig.images, + ...themeConfig?.images, + }, }; }, [inheritFromBranding, brandingTheme, themeConfig]); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 0eae6cac9..c7b9e73aa 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -239,6 +239,9 @@ export * from './components/primitives/Typography/Typography'; export {default as Divider} from './components/primitives/Divider/Divider'; export * from './components/primitives/Divider/Divider'; +export {default as Logo} from './components/primitives/Logo/Logo'; +export * from './components/primitives/Logo/Logo'; + export {default as Spinner} from './components/primitives/Spinner/Spinner'; export * from './components/primitives/Spinner/Spinner'; diff --git a/samples/teamspace-react/src/pages/SignInPage.tsx b/samples/teamspace-react/src/pages/SignInPage.tsx index 8c561c524..455880d04 100644 --- a/samples/teamspace-react/src/pages/SignInPage.tsx +++ b/samples/teamspace-react/src/pages/SignInPage.tsx @@ -7,18 +7,7 @@ export default function SignInPage() { return (
- {/* Header */} - - -
← Back to home diff --git a/samples/teamspace-react/src/pages/SignUpPage.tsx b/samples/teamspace-react/src/pages/SignUpPage.tsx index 18dbfc199..c5eb730d0 100644 --- a/samples/teamspace-react/src/pages/SignUpPage.tsx +++ b/samples/teamspace-react/src/pages/SignUpPage.tsx @@ -10,18 +10,7 @@ export default function SignUpPage() { return (
- {/* Header */} - - navigate('/signin')} /> -
← Back to home From c51a9c0992f5ab1de0ae420baff99473b14b96cc Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 4 Jul 2025 02:07:45 +0530 Subject: [PATCH 30/31] feat(react): enhance BaseSignIn component with custom styles and improved layout --- .../presentation/SignIn/BaseSignIn.tsx | 224 +++++++++--------- .../presentation/SignUp/BaseSignUp.tsx | 137 +++++++---- 2 files changed, 197 insertions(+), 164 deletions(-) diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx index 0e6335b6b..6552bc5ab 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -30,7 +30,7 @@ import { EmbeddedFlowExecuteRequestConfig, } from '@asgardeo/browser'; import {clsx} from 'clsx'; -import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef} from 'react'; +import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef, useMemo, CSSProperties} from 'react'; import {createSignInOptionFromAuthenticator} from './options/SignInOptionFactory'; import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; @@ -285,6 +285,71 @@ export interface BaseSignInProps { variant?: CardProps['variant']; } +/** + * Custom hook for managing component styles + */ +const useStyles = () => { + const {theme} = useTheme(); + + return useMemo( + () => ({ + card: { + gap: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + header: { + gap: 0, + } as CSSProperties, + subtitle: { + marginTop: `calc(${theme.vars.spacing.unit} * 1)`, + } as CSSProperties, + messagesContainer: { + marginTop: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + messageItem: { + marginBottom: `calc(${theme.vars.spacing.unit} * 1)`, + } as CSSProperties, + errorContainer: { + marginBottom: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + contentContainer: { + display: 'flex', + flexDirection: 'column', + gap: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + loadingContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: `calc(${theme.vars.spacing.unit} * 4)`, + } as CSSProperties, + loadingText: { + marginTop: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + divider: { + margin: `calc(${theme.vars.spacing.unit} * 1) 0`, + } as CSSProperties, + logoContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginBottom: `calc(${theme.vars.spacing.unit} * 3)`, + } as CSSProperties, + centeredContainer: { + textAlign: 'center', + padding: `calc(${theme.vars.spacing.unit} * 4)`, + } as CSSProperties, + passkeyContainer: { + marginBottom: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + passkeyText: { + marginTop: `calc(${theme.vars.spacing.unit} * 1)`, + color: theme.vars.colors.text.secondary, + } as CSSProperties, + }), + [theme.vars.spacing.unit, theme.vars.colors.text.secondary], + ); +}; + /** * Base SignIn component that provides native authentication flow. * This component handles both the presentation layer and authentication flow logic. @@ -317,11 +382,21 @@ export interface BaseSignInProps { * }; * ``` */ -const BaseSignIn: FC = props => ( - - - -); +const BaseSignIn: FC = props => { + const {theme} = useTheme(); + const styles = useStyles(); + + return ( +
+
+ +
+ + + +
+ ); +}; /** * Internal component that consumes FlowContext and renders the sign-in UI. @@ -345,6 +420,7 @@ const BaseSignInContent: FC = ({ const {theme} = useTheme(); const {t} = useTranslation(); const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); + const styles = useStyles(); const [isSignInInitializationRequestLoading, setIsSignInInitializationRequestLoading] = useState(false); const [isInitialized, setIsInitialized] = useState(false); @@ -1093,30 +1169,11 @@ const BaseSignInContent: FC = ({ if (!isInitialized && isLoading) { return ( - - -
- -
-
+ -
+
- + {t('messages.loading')}
@@ -1138,31 +1195,21 @@ const BaseSignInContent: FC = ({ const optionAuthenticators = availableAuthenticators.filter(auth => !userPromptAuthenticators.includes(auth)); return ( - - -
- -
- {flowTitle || t('signin.title')} + + + {flowTitle || t('signin.title')} {flowSubtitle && ( - + {flowSubtitle || t('signin.subtitle')} )} {flowMessages && flowMessages.length > 0 && ( -
+
{flowMessages.map((flowMessage, index) => ( {flowMessage.message} @@ -1171,7 +1218,7 @@ const BaseSignInContent: FC = ({
)} {messages.length > 0 && ( -
+
{messages.map((message, index) => { const variant = message.type.toLowerCase() === 'error' @@ -1183,12 +1230,7 @@ const BaseSignInContent: FC = ({ : 'info'; return ( - + {message.message} ); @@ -1199,21 +1241,17 @@ const BaseSignInContent: FC = ({ {error && ( - + Error {error} )} -
+
{/* Render USER_PROMPT authenticators as form fields */} {userPromptAuthenticators.map((authenticator, index) => (
- {index > 0 && OR} + {index > 0 && OR}
{ e.preventDefault(); @@ -1243,7 +1281,7 @@ const BaseSignInContent: FC = ({ {/* Add divider between user prompts and option authenticators if both exist */} {userPromptAuthenticators.length > 0 && optionAuthenticators.length > 0 && ( - OR + OR )} {/* Render all other authenticators (REDIRECTION_PROMPT, multi-option buttons, etc.) */} @@ -1273,18 +1311,6 @@ const BaseSignInContent: FC = ({ if (!currentAuthenticator) { return ( - -
- -
-
{error && ( @@ -1307,28 +1333,13 @@ const BaseSignInContent: FC = ({ // Show loading state while passkey authentication is in progress return ( - -
- -
-
-
-
+
+
{t('passkey.authenticating') || 'Authenticating with passkey...'} - + {t('passkey.instruction') || 'Please use your fingerprint, face, or security key to authenticate.'}
@@ -1338,29 +1349,19 @@ const BaseSignInContent: FC = ({ } return ( - - -
- -
+ + {flowTitle || t('signin.title')} - + {flowSubtitle || t('signin.subtitle')} {flowMessages && flowMessages.length > 0 && ( -
+
{flowMessages.map((flowMessage, index) => ( {flowMessage.message} @@ -1369,7 +1370,7 @@ const BaseSignInContent: FC = ({
)} {messages.length > 0 && ( -
+
{messages.map((message, index) => { const variant = message.type.toLowerCase() === 'error' @@ -1381,12 +1382,7 @@ const BaseSignInContent: FC = ({ : 'info'; return ( - + {message.message} ); @@ -1397,11 +1393,7 @@ const BaseSignInContent: FC = ({ {error && ( - + {t('errors.title')} {error} diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 6b6d91199..d9f805669 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -26,7 +26,7 @@ import { AsgardeoAPIError, } from '@asgardeo/browser'; import {clsx} from 'clsx'; -import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef} from 'react'; +import {FC, ReactElement, FormEvent, useEffect, useState, useCallback, useRef, useMemo, CSSProperties} from 'react'; import {renderSignUpComponents} from './options/SignUpOptionFactory'; import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; @@ -122,6 +122,71 @@ export interface BaseSignUpProps { shouldRedirectAfterSignUp?: boolean; } +/** + * Custom hook for managing component styles + */ +const useStyles = () => { + const {theme} = useTheme(); + + return useMemo( + () => ({ + card: { + gap: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + header: { + gap: 0, + } as CSSProperties, + subtitle: { + marginTop: `calc(${theme.vars.spacing.unit} * 1)`, + } as CSSProperties, + messagesContainer: { + marginTop: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + messageItem: { + marginBottom: `calc(${theme.vars.spacing.unit} * 1)`, + } as CSSProperties, + errorContainer: { + marginBottom: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + contentContainer: { + display: 'flex', + flexDirection: 'column', + gap: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + loadingContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: `calc(${theme.vars.spacing.unit} * 4)`, + } as CSSProperties, + loadingText: { + marginTop: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + divider: { + margin: `calc(${theme.vars.spacing.unit} * 1) 0`, + } as CSSProperties, + logoContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginBottom: `calc(${theme.vars.spacing.unit} * 3)`, + } as CSSProperties, + centeredContainer: { + textAlign: 'center', + padding: `calc(${theme.vars.spacing.unit} * 4)`, + } as CSSProperties, + passkeyContainer: { + marginBottom: `calc(${theme.vars.spacing.unit} * 2)`, + } as CSSProperties, + passkeyText: { + marginTop: `calc(${theme.vars.spacing.unit} * 1)`, + color: theme.vars.colors.text.secondary, + } as CSSProperties, + }), + [theme.vars.spacing.unit, theme.vars.colors.text.secondary], + ); +}; + /** * Base SignUp component that provides embedded sign-up flow. * This component handles both the presentation layer and sign-up flow logic. @@ -157,11 +222,21 @@ export interface BaseSignUpProps { * }; * ``` */ -const BaseSignUp: FC = props => ( - - - -); +const BaseSignUp: FC = props => { + const {theme} = useTheme(); + const styles = useStyles(); + + return ( +
+
+ +
+ + + +
+ ); +}; /** * Internal component that consumes FlowContext and renders the sign-up UI. @@ -185,6 +260,7 @@ const BaseSignUpContent: FC = ({ const {theme} = useTheme(); const {t} = useTranslation(); const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); + const styles = useStyles(); const [isLoading, setIsLoading] = useState(false); const [isFlowInitialized, setIsFlowInitialized] = useState(false); @@ -635,19 +711,7 @@ const BaseSignUpContent: FC = ({ if (!isFlowInitialized && isLoading) { return ( - - -
- -
-
+
@@ -659,19 +723,7 @@ const BaseSignUpContent: FC = ({ if (!currentFlow) { return ( - - -
- -
-
+ {t('errors.title') || 'Error'} @@ -683,19 +735,9 @@ const BaseSignUpContent: FC = ({ } return ( - - -
- -
- {flowMessages && flowMessages.length > 0 && ( + + {flowMessages && flowMessages.length > 0 && ( +
{flowMessages.map((message: any, index: number) => ( = ({ ))}
- )} -
- +
+ )} {error && ( Date: Fri, 4 Jul 2025 02:08:40 +0530 Subject: [PATCH 31/31] chore: add changeset --- .changeset/tiny-worms-repeat.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/tiny-worms-repeat.md diff --git a/.changeset/tiny-worms-repeat.md b/.changeset/tiny-worms-repeat.md new file mode 100644 index 000000000..cd5a46220 --- /dev/null +++ b/.changeset/tiny-worms-repeat.md @@ -0,0 +1,11 @@ +--- +'@asgardeo/browser': patch +'@asgardeo/express': patch +'@asgardeo/javascript': patch +'@asgardeo/nextjs': patch +'@asgardeo/node': patch +'@asgardeo/react': patch +'@asgardeo/vue': patch +--- + +Fix B2B components
+
{formatLabel(key)}: + {typeof value === 'object' ? : String(value)}