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
1 change: 1 addition & 0 deletions packages/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export type {
ValidationRule,
ValidationRuleType,
FieldError,
PrefixOption,
} from './models/embedded-flow';
export {EmbeddedSignInFlowStatus, EmbeddedSignInFlowType} from './models/embedded-signin-flow';
export type {
Expand Down
15 changes: 15 additions & 0 deletions packages/javascript/src/models/embedded-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,10 @@ export interface EmbeddedFlowComponent {
*/
placeholder?: string;

postfix?: string;

prefixes?: string | PrefixOption[];

/**
* Reference identifier for the component (e.g., field name, action ref)
*/
Expand Down Expand Up @@ -508,6 +512,17 @@ export interface FieldError {
message: string;
}

export interface PrefixOption {
/** The value of the prefix option (e.g., country code, currency symbol). */
value: string;
/** Display label for the prefix option (e.g., "India (+91)"). */
label: string;
/** Optional maximum length constraint for input when this prefix is selected. */
maxLength?: number;
/** Optional regex pattern for validating input when this prefix is selected. */
regex?: string;
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
/**
* Response data structure for embedded flow API.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ import {
ConsentPurposeDecision,
ConsentAttributeElement,
OrganizationUnitListResponse,
PrefixOption,
} from '@thunderid/browser';
import DOMPurify from 'dompurify';
import {cloneElement, CSSProperties, ReactElement} from 'react';
import {ChangeEvent, cloneElement, CSSProperties, ReactElement} from 'react';
import {
ComponentRenderer,
ComponentRenderContext,
Expand All @@ -60,7 +61,9 @@ import CopyableText from '../../primitives/CopyableText/CopyableText';
import DatePicker from '../../primitives/DatePicker/DatePicker';
import Divider from '../../primitives/Divider/Divider';
import flowIconRegistry from '../../primitives/Icons/flowIconRegistry';
import AffixedField from '../../primitives/AffixedField/AffixedField';
import Select from '../../primitives/Select/Select';
import {affixPostfixKey, affixPrefixKey} from '../../../utils/composeAffixedInputs';
import Typography from '../../primitives/Typography/Typography';
import {TypographyVariant} from '../../primitives/Typography/Typography.styles';

Expand Down Expand Up @@ -212,6 +215,8 @@ const createAuthComponentFromFlow = (
/** Translation function for resolving {{t(...)}} expressions at render time */
t?: UseTranslation['t'];
variant?: any;
/** Vendor namespace used to derive affix sentinel keys for prefix/postfix-bearing inputs. */
vendor?: string;
} = {},
): ReactElement | null => {
const theme: any = options._theme;
Expand Down Expand Up @@ -257,6 +262,53 @@ const createAuthComponentFromFlow = (
const error: string = isTouched ? formErrors[identifier] : undefined!;
const fieldType: string = getFieldType(component.type);

// Prefix/postfix are accepted on any text-family input. When either is present,
// route to AffixedField so the leading affix renders and submit-time composition
// is picked up by composeAffixedInputs via the sentinel keys.
// Both string.length and Array.length are defined, so a single truthy+length
// check covers `prefixes?: string | PrefixOption[]`.
const hasPrefixes: boolean = !!component.prefixes && component.prefixes.length > 0;
const hasPostfix: boolean = typeof component.postfix === 'string' && component.postfix.length > 0;

if (hasPrefixes || hasPostfix) {
const prefixStateKey: string = affixPrefixKey(identifier, options.vendor);
const postfixStateKey: string = affixPostfixKey(identifier, options.vendor);
const defaultPrefixValue: string = hasPrefixes
? typeof component.prefixes === 'string'
? component.prefixes
: (component.prefixes as PrefixOption[])[0].value
: '';
const selectedPrefix: string = formValues[prefixStateKey] ?? defaultPrefixValue;

return (
<AffixedField
key={key}
className={cx(options.inputClassName, component.classes)}
id={component.id}
label={resolve(component.label) || ''}
name={identifier}
placeholder={resolve(component.placeholder) || ''}
required={component.required || false}
error={error}
value={value}
type={fieldType}
prefixes={component.prefixes}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
prefixValue={selectedPrefix}
onChange={(e: ChangeEvent<HTMLInputElement>): void => onInputChange(identifier, e.target.value)}
onBlur={(): void => options.onInputBlur?.(identifier)}
onPrefixChange={(newPrefix: string): void => onInputChange(prefixStateKey, newPrefix)}
onFocus={(): void => {
if (hasPrefixes && formValues[prefixStateKey] === undefined) {
onInputChange(prefixStateKey, defaultPrefixValue);
}
if (hasPostfix && formValues[postfixStateKey] === undefined) {
onInputChange(postfixStateKey, component.postfix!);
}
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/>
);
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const field: any = createField({
className: cx(options.inputClassName, component.classes),
error,
Expand Down Expand Up @@ -756,6 +808,8 @@ export const renderSignInComponents = (
/** Translation function for resolving {{t(...)}} expressions at render time */
t?: UseTranslation['t'];
variant?: any;
/** Vendor namespace used to derive affix sentinel keys for prefix/postfix-bearing inputs. */
vendor?: string;
},
): ReactElement[] =>
components
Expand Down Expand Up @@ -805,6 +859,8 @@ export const renderSignUpComponents = (
/** Translation function for resolving {{t(...)}} expressions at render time */
t?: UseTranslation['t'];
variant?: any;
/** Vendor namespace used to derive affix sentinel keys for prefix/postfix-bearing inputs. */
vendor?: string;
},
): ReactElement[] =>
components
Expand Down Expand Up @@ -856,6 +912,8 @@ export const renderRecoveryComponents = (
/** Translation function for resolving {{t(...)}} expressions at render time */
t?: UseTranslation['t'];
variant?: any;
/** Vendor namespace used to derive affix sentinel keys for prefix/postfix-bearing inputs. */
vendor?: string;
},
): ReactElement[] =>
components
Expand Down Expand Up @@ -912,6 +970,8 @@ export const renderInviteUserComponents = (
/** Translation function for resolving {{t(...)}} expressions at render time */
t?: UseTranslation['t'];
variant?: any;
/** Vendor namespace used to derive affix sentinel keys for prefix/postfix-bearing inputs. */
vendor?: string;
},
): ReactElement[] =>
components
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import useTheme from '../../../../contexts/Theme/useTheme';
import useThunderID from '../../../../contexts/ThunderID/useThunderID';
import {useForm, FormField} from '../../../../hooks/useForm';
import useTranslation from '../../../../hooks/useTranslation';
import composeAffixedInputs from '../../../../utils/composeAffixedInputs';
import {normalizeFlowResponse, extractErrorMessage} from '../../../../utils/flowTransformer';
import getAuthComponentHeadings from '../../../../utils/getAuthComponentHeadings';
import AlertPrimitive from '../../../primitives/Alert/Alert';
Expand Down Expand Up @@ -129,7 +130,7 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);
const {t} = useTranslation();
const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages} = useFlow();
const {meta} = useThunderID();
const {meta, vendor} = useThunderID();
const styles: any = useStyles(theme, colorScheme);

const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -310,9 +311,13 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
clearMessages();

try {
const composedData: Record<string, any> | undefined = data
? composeAffixedInputs(data as Record<string, string>, vendor)
: undefined;

const filteredInputs: Record<string, any> = {};
if (data) {
Object.entries(data).forEach(([key, value]: [string, any]) => {
if (composedData) {
Object.entries(composedData).forEach(([key, value]: [string, any]) => {
if (value !== null && value !== undefined && value !== '') {
filteredInputs[key] = value;
}
Expand Down Expand Up @@ -414,6 +419,7 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
onSubmit: handleSubmit,
size,
variant,
vendor,
},
),
[
Expand All @@ -431,6 +437,7 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
theme,
touchedFields,
variant,
vendor,
],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import useTheme from '../../../../contexts/Theme/useTheme';
import useThunderID from '../../../../contexts/ThunderID/useThunderID';
import {FormField, useForm} from '../../../../hooks/useForm';
import useTranslation from '../../../../hooks/useTranslation';
import composeAffixedInputs from '../../../../utils/composeAffixedInputs';
import {extractErrorMessage} from '../../../../utils/flowTransformer';
import AlertPrimitive from '../../../primitives/Alert/Alert';
// eslint-disable-next-line import/no-named-as-default
Expand Down Expand Up @@ -257,7 +258,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
isTimeoutDisabled = false,
serverFieldErrors = null,
}: BaseSignInProps): ReactElement => {
const {meta} = useThunderID();
const {meta, vendor} = useThunderID();
const {theme} = useTheme();
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);
const {t} = useTranslation();
Expand Down Expand Up @@ -439,12 +440,16 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
clearMessages();

try {
const composedData: Record<string, any> | undefined = data
? composeAffixedInputs(data as Record<string, string>, vendor)
: undefined;

// Filter out empty or undefined input values
const filteredInputs: Record<string, any> = {};
if (data) {
Object.keys(data).forEach((key: any) => {
if (data[key] !== undefined && data[key] !== null && data[key] !== '') {
filteredInputs[key] = data[key];
if (composedData) {
Object.keys(composedData).forEach((key: any) => {
if (composedData[key] !== undefined && composedData[key] !== null && composedData[key] !== '') {
filteredInputs[key] = composedData[key];
}
});
}
Expand Down Expand Up @@ -523,6 +528,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
size,
t,
variant,
vendor,
},
),
[
Expand All @@ -538,6 +544,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
isLoading,
size,
variant,
vendor,
inputClasses,
buttonClasses,
handleInputBlur,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import useTheme from '../../../../contexts/Theme/useTheme';
import useThunderID from '../../../../contexts/ThunderID/useThunderID';
import {useForm, FormField} from '../../../../hooks/useForm';
import useTranslation from '../../../../hooks/useTranslation';
import composeAffixedInputs from '../../../../utils/composeAffixedInputs';
import {normalizeFlowResponse, extractErrorMessage} from '../../../../utils/flowTransformer';
import getAuthComponentHeadings from '../../../../utils/getAuthComponentHeadings';
import {handlePasskeyRegistration} from '../../../../utils/passkey';
Expand Down Expand Up @@ -286,7 +287,7 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);
const {t} = useTranslation();
const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages} = useFlow();
const {meta, isInitialized: isSdkInitialized, getStorageManager} = useThunderID();
const {meta, isInitialized: isSdkInitialized, getStorageManager, vendor} = useThunderID();
const styles: any = useStyles(theme, colorScheme);

const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -796,10 +797,14 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
clearMessages();

try {
const composedData: Record<string, any> | undefined = data
? composeAffixedInputs(data as Record<string, string>, vendor)
: undefined;

// Filter out empty or undefined input values
const filteredInputs: Record<string, any> = {};
if (data) {
Object.entries(data).forEach(([key, value]: [string, any]) => {
if (composedData) {
Object.entries(composedData).forEach(([key, value]: [string, any]) => {
if (value !== null && value !== undefined && value !== '') {
filteredInputs[key] = value;
}
Expand Down Expand Up @@ -978,6 +983,7 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
onSubmit: handleSubmit,
size,
variant,
vendor,
},
),
[
Expand All @@ -990,6 +996,7 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
size,
theme,
variant,
vendor,
inputClasses,
buttonClasses,
handleSubmit,
Expand Down
Loading
Loading