diff --git a/.changeset/shiny-suits-occur.md b/.changeset/shiny-suits-occur.md new file mode 100644 index 00000000..b30bf6e3 --- /dev/null +++ b/.changeset/shiny-suits-occur.md @@ -0,0 +1,6 @@ +--- +'@asgardeo/browser': patch +'@asgardeo/react': patch +--- + +Fix issues in Language Dropdown & expose emoji resolving utils diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index efbcdedf..7baac241 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -67,3 +67,4 @@ export {default as getActiveTheme} from './theme/getActiveTheme'; export {default as handleWebAuthnAuthentication} from './utils/handleWebAuthnAuthentication'; export {default as http} from './utils/http'; +export {default as resolveEmojiUrisInHtml} from './utils/v2/resolveEmojiUrisInHtml'; diff --git a/packages/browser/src/utils/v2/resolveEmojiUrisInHtml.ts b/packages/browser/src/utils/v2/resolveEmojiUrisInHtml.ts new file mode 100644 index 00000000..9c8f8dcc --- /dev/null +++ b/packages/browser/src/utils/v2/resolveEmojiUrisInHtml.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026, 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 {extractEmojiFromUri, isEmojiUri} from '@asgardeo/javascript'; + +/** + * Resolves `emoji:` URIs in an HTML string. + * + * Handles two forms: + * - `tiger` → `🐯` + * - Bare `emoji:🐯` text references → `🐯` + * + * @param html - The HTML string that may contain `emoji:` URIs. + * @returns The HTML string with all `emoji:` URIs replaced. + */ +const resolveEmojiUrisInHtml = (html: string): string => { + const withEmojiImages: string = html.replace( + /]*)src="(emoji:[^"]+)"([^>]*)\/?>/gi, + (_match: string, pre: string, src: string, post: string): string => { + const emoji: string = extractEmojiFromUri(src); + if (!emoji) { + return _match; + } + const altMatch: RegExpMatchArray | null = (pre + post).match(/alt="([^"]*)"/i); + const label: string = altMatch ? altMatch[1] : emoji; + return `${emoji}`; + }, + ); + return withEmojiImages.replace(/emoji:([^\s"<>&]+)/g, (_: string, rest: string): string => + isEmojiUri(`emoji:${rest}`) ? rest : `emoji:${rest}`, + ); +}; + +export default resolveEmojiUrisInHtml; diff --git a/packages/react/src/components/presentation/LanguageSwitcher/LanguageSwitcher.tsx b/packages/react/src/components/presentation/LanguageSwitcher/LanguageSwitcher.tsx index 399f407f..1a86d55f 100644 --- a/packages/react/src/components/presentation/LanguageSwitcher/LanguageSwitcher.tsx +++ b/packages/react/src/components/presentation/LanguageSwitcher/LanguageSwitcher.tsx @@ -17,7 +17,7 @@ */ import {resolveLocaleDisplayName, resolveLocaleEmoji} from '@asgardeo/browser'; -import {FC, ReactElement, ReactNode, useMemo} from 'react'; +import {FC, ReactElement, ReactNode, useEffect, useMemo} from 'react'; import BaseLanguageSwitcher, {LanguageOption, LanguageSwitcherRenderProps} from './BaseLanguageSwitcher'; import useFlowMeta from '../../../contexts/FlowMeta/useFlowMeta'; import useTranslation from '../../../hooks/useTranslation'; @@ -85,12 +85,13 @@ const LanguageSwitcher: FC = ({children, className}: Lang const {currentLanguage} = useTranslation(); const availableLanguageCodes: string[] = meta?.i18n?.languages ?? []; - const effectiveLanguageCodes: string[] = useMemo(() => { - const fallbackCodes: string[] = availableLanguageCodes.length > 0 ? availableLanguageCodes : [currentLanguage]; - - // Ensure the current language is always resolvable for display label and emoji. - return Array.from(new Set([currentLanguage, ...fallbackCodes])); - }, [availableLanguageCodes, currentLanguage]); + // Only fall back to the detected browser language when the server returns no configured languages. + // Do NOT inject currentLanguage unconditionally — a browser locale like "en-GB" must not appear + // in the picker when the server only supports "en-US". + const effectiveLanguageCodes: string[] = useMemo( + () => (availableLanguageCodes.length > 0 ? availableLanguageCodes : [currentLanguage]), + [availableLanguageCodes, currentLanguage], + ); const languages: LanguageOption[] = useMemo( () => @@ -103,6 +104,13 @@ const LanguageSwitcher: FC = ({children, className}: Lang [effectiveLanguageCodes], ); + // If the detected language isn't supported by the server, fall back to the first available language. + useEffect(() => { + if (availableLanguageCodes.length > 0 && !availableLanguageCodes.includes(currentLanguage)) { + switchLanguage(availableLanguageCodes[0]); + } + }, [availableLanguageCodes, currentLanguage, switchLanguage]); + const handleLanguageChange = (language: string): void => { if (language !== currentLanguage) { switchLanguage(language); diff --git a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx index fc9feb21..5a607c58 100644 --- a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx +++ b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx @@ -25,8 +25,7 @@ import { EmbeddedFlowEventTypeV2 as EmbeddedFlowEventType, createPackageComponentLogger, resolveFlowTemplateLiterals, - extractEmojiFromUri, - isEmojiUri, + resolveEmojiUrisInHtml, ConsentPurposeDataV2 as ConsentPurposeData, ConsentPromptDataV2 as ConsentPromptData, ConsentDecisionsV2 as ConsentDecisions, @@ -36,6 +35,7 @@ import { import {css} from '@emotion/css'; import DOMPurify from 'dompurify'; import {cloneElement, CSSProperties, ReactElement} from 'react'; +import useTheme from '../../../contexts/Theme/useTheme'; import {UseTranslation} from '../../../hooks/useTranslation'; import Consent from '../../adapters/Consent'; import {getConsentOptionalKey} from '../../adapters/ConsentCheckboxList'; @@ -69,23 +69,6 @@ const logger: ReturnType = createPackageCom * - `Y` → `X` * - Any remaining `emoji:X` text occurrences → `X` */ -const resolveEmojiUrisInHtml = (html: string): string => { - const withEmojiImages: string = html.replace( - /]*)src="(emoji:[^"]+)"([^>]*)\/?>/gi, - (_match: string, pre: string, src: string, post: string): string => { - const emoji: string = extractEmojiFromUri(src); - if (!emoji) { - return _match; - } - const altMatch: RegExpMatchArray | null = (pre + post).match(/alt="([^"]*)"/i); - const label: string = altMatch ? altMatch[1] : emoji; - return `${emoji}`; - }, - ); - return withEmojiImages.replace(/emoji:([^\s"<>&]+)/g, (_: string, rest: string): string => - isEmojiUri(`emoji:${rest}`) ? rest : `emoji:${rest}`, - ); -}; /** Ensures rich-text content (including all inner elements from the server) always word-wraps. */ const richTextClass: string = css` @@ -217,6 +200,8 @@ const createAuthComponentFromFlow = ( variant?: any; } = {}, ): ReactElement | null => { + const {theme} = useTheme(); + const key: string | number = options.key || component.id; /** Resolve any remaining {{t()}} or {{meta()}} template expressions in a string at render time. */ @@ -421,6 +406,12 @@ const createAuthComponentFromFlow = ( case EmbeddedFlowComponentType.Block: { if (component.components && component.components.length > 0) { + const formStyles: CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: `calc(${theme.vars.spacing.unit} * 2)`, + }; + const blockComponents: any = component.components .map((childComponent: any, index: any) => createAuthComponentFromFlow( @@ -441,7 +432,7 @@ const createAuthComponentFromFlow = ( .filter(Boolean); return ( -
+ {blockComponents}
); diff --git a/packages/react/src/components/primitives/FormControl/FormControl.styles.ts b/packages/react/src/components/primitives/FormControl/FormControl.styles.ts index 6441f725..858a4c69 100644 --- a/packages/react/src/components/primitives/FormControl/FormControl.styles.ts +++ b/packages/react/src/components/primitives/FormControl/FormControl.styles.ts @@ -42,7 +42,6 @@ const useStyles = ( const formControl: string = css` text-align: start; font-family: ${theme.vars.typography.fontFamily}; - margin-bottom: calc(${theme.vars.spacing.unit} * 2); `; const helperText: string = css` diff --git a/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx b/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx index 3c7fbd09..8ef264ee 100644 --- a/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx +++ b/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx @@ -18,20 +18,11 @@ import {FlowMetadataResponse, FlowMetaType, getFlowMetaV2} from '@asgardeo/browser'; import {I18nBundle, TranslationBundleConstants} from '@asgardeo/i18n'; -import { - FC, - PropsWithChildren, - ReactElement, - RefObject, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from 'react'; +import {FC, PropsWithChildren, ReactElement, RefObject, useCallback, useEffect, useRef, useState} from 'react'; import FlowMetaContext from './FlowMetaContext'; import useAsgardeo from '../Asgardeo/useAsgardeo'; -import I18nContext, {I18nContextValue} from '../I18n/I18nContext'; +import {I18nContextValue} from '../I18n/I18nContext'; +import useI18n from '../I18n/useI18n'; export interface FlowMetaProviderProps { /** @@ -69,7 +60,7 @@ const FlowMetaProvider: FC> = ({ enabled = true, }: PropsWithChildren): ReactElement => { const {baseUrl, applicationId} = useAsgardeo(); - const i18nContext: I18nContextValue = useContext(I18nContext); + const i18nContext: I18nContextValue = useI18n(); const [meta, setMeta] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -90,14 +81,19 @@ const FlowMetaProvider: FC> = ({ setError(null); try { - const result: FlowMetadataResponse = await getFlowMetaV2({baseUrl, id: applicationId, type: FlowMetaType.App}); + const result: FlowMetadataResponse = await getFlowMetaV2({ + baseUrl, + id: applicationId, + language: i18nContext?.currentLanguage, + type: FlowMetaType.App, + }); setMeta(result); } catch (err: unknown) { setError(err instanceof Error ? err : new Error(String(err))); } finally { setIsLoading(false); } - }, [enabled, baseUrl, applicationId]); + }, [enabled, baseUrl, applicationId, i18nContext?.currentLanguage]); const switchLanguage: (language: string) => Promise = useCallback( async (language: string): Promise => { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index a47abe7b..37d1b495 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -289,6 +289,15 @@ export { http, getActiveTheme, navigate, + resolveEmojiUrisInHtml, + isEmojiUri, + EMOJI_URI_SCHEME, + extractEmojiFromUri, + resolveMeta, + resolveFlowTemplateLiterals, + countryCodeToFlagEmoji, + resolveLocaleDisplayName, + resolveLocaleEmoji, // Export `v2` models and types as first class citizens since they are // going to be the primary way to interact with embedded flows moving forward. EmbeddedFlowComponentTypeV2 as EmbeddedFlowComponentType,