Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/shiny-suits-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@asgardeo/browser': patch
'@asgardeo/react': patch
---

Fix issues in Language Dropdown & expose emoji resolving utils
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
49 changes: 49 additions & 0 deletions packages/browser/src/utils/v2/resolveEmojiUrisInHtml.ts
Original file line number Diff line number Diff line change
@@ -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:
* - `<img src="emoji:🐯" alt="tiger">` → `<span role="img" aria-label="tiger">🐯</span>`
* - 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(
/<img([^>]*)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 `<span role="img" aria-label="${label}">${emoji}</span>`;
},
);
return withEmojiImages.replace(/emoji:([^\s"<>&]+)/g, (_: string, rest: string): string =>
isEmojiUri(`emoji:${rest}`) ? rest : `emoji:${rest}`,
);
};

export default resolveEmojiUrisInHtml;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -85,12 +85,13 @@ const LanguageSwitcher: FC<LanguageSwitcherProps> = ({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(
() =>
Expand All @@ -103,6 +104,13 @@ const LanguageSwitcher: FC<LanguageSwitcherProps> = ({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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import {
EmbeddedFlowEventTypeV2 as EmbeddedFlowEventType,
createPackageComponentLogger,
resolveFlowTemplateLiterals,
extractEmojiFromUri,
isEmojiUri,
resolveEmojiUrisInHtml,
ConsentPurposeDataV2 as ConsentPurposeData,
ConsentPromptDataV2 as ConsentPromptData,
ConsentDecisionsV2 as ConsentDecisions,
Expand All @@ -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';
Expand Down Expand Up @@ -69,23 +69,6 @@ const logger: ReturnType<typeof createPackageComponentLogger> = createPackageCom
* - `<img src="emoji:X" alt="Y">` → `<span role="img" aria-label="Y">X</span>`
* - Any remaining `emoji:X` text occurrences → `X`
*/
const resolveEmojiUrisInHtml = (html: string): string => {
const withEmojiImages: string = html.replace(
/<img([^>]*)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 `<span role="img" aria-label="${label}">${emoji}</span>`;
},
);
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`
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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(
Expand All @@ -441,7 +432,7 @@ const createAuthComponentFromFlow = (
.filter(Boolean);

return (
<form id={component.id} key={key}>
<form id={component.id} key={key} style={formStyles}>
{blockComponents}
</form>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
26 changes: 11 additions & 15 deletions packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -69,7 +60,7 @@ const FlowMetaProvider: FC<PropsWithChildren<FlowMetaProviderProps>> = ({
enabled = true,
}: PropsWithChildren<FlowMetaProviderProps>): ReactElement => {
const {baseUrl, applicationId} = useAsgardeo();
const i18nContext: I18nContextValue = useContext(I18nContext);
const i18nContext: I18nContextValue = useI18n();

const [meta, setMeta] = useState<FlowMetadataResponse | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
Expand All @@ -90,14 +81,19 @@ const FlowMetaProvider: FC<PropsWithChildren<FlowMetaProviderProps>> = ({
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<void> = useCallback(
async (language: string): Promise<void> => {
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading