Multiple LOGIN_ID_INPUT Flow Component #2136
Replies: 3 comments
-
|
Hi @anushasunkada, please find the diagram for the suggested design
|
Beta Was this translation helpful? Give feedback.
-
|
@anushasunkada following up on our discussion, I think flow definition will be like this with both multi option and multi login ID support. Technically this should work. Please note that I didn't try to repeat prompt node definitions in the previous reply. This will have the same definitions.
|
Beta Was this translation helpful? Give feedback.
-
Phone Number Input with Country Code - ImplementationWe're extending How data flows through the systemBefore jumping into code, it helps to understand the path a value takes:
How country code options workThe country code list is configured by the flow author in the flow builder UI. The console provides a way to search and pick countries, and the author controls:
This configuration gets saved as part of the flow definition. When a user hits the login page, the backend sends the flow definition to the SDK, and the SDK adapter renders the options exactly as configured. The adapter doesn't need to know what countries exist in the world. It just iterates the options array and renders them. The console will need a country picker UI in the properties panel for this component. This is where you'd have a searchable list of countries with their flag emojis and dial codes as a convenience for the flow author. Think of it as a preset library that helps populate the options array, not a runtime dependency. The console can bundle a reference dataset of countries/flags/dial codes to power that picker. The SDK never sees that dataset. The resulting component definition in the flow looks like: {
"ref": "mobileNumber",
"type": "PHONE_NUMBER_INPUT",
"required": true,
"options": [
{ "label": "🇱🇰 Sri Lanka (+94)", "value": "+94" },
{ "label": "🇺🇸 United States (+1)", "value": "+1" },
{ "label": "🇮🇳 India (+91)", "value": "+91" }
],
"defaultValue": "+94"
}Backend changesAdd the constant in // InputTypePhoneNumber represents a phone number input with country code selection.
InputTypePhoneNumber = "PHONE_NUMBER_INPUT"The Input model already has an Validation: The submitted value arrives as a single E.164 string (e.g., Flow builder changes (console)The existing
This is a separate piece of UI work in the console, but the underlying data shape is just an array of SDK adapter (thunder-design)Currently, Create A few things worth keeping in mind while building this: Branch on whether options exist. If The options come from the component definition. Use the same When there's only one country code option, skip the dropdown. Just show the code as static text. No point making the user interact with a single-item dropdown. Strip non-digit characters from the number field on change. The Don't submit a bare country code. If the user hasn't typed a number yet, send an empty string to the form, not Use Here's a sketch of the component structure: export default function PhoneNumberInputAdapter({
component, values, touched, fieldErrors, isLoading, resolve, onInputChange,
}: FlowFieldProps): JSX.Element | null {
const {ref, options} = component;
if (!ref || typeof ref !== 'string') return null;
const hasCountryCodes = options && options.length > 0;
// No country codes configured — render a plain tel input
// (same behavior as the current TextInputAdapter for PHONE_INPUT)
if (!hasCountryCodes) {
return (
<FormControl required={component.required}>
<FormLabel htmlFor={ref}>{t(resolve(component.label)!)}</FormLabel>
<TextField
type="tel"
autoComplete="tel"
value={values[ref] ?? ''}
onChange={(e) => onInputChange(ref, e.target.value)}
...
/>
</FormControl>
);
}
// Country codes configured — render dropdown + number field
const defaultCode = component.defaultValue
? String(component.defaultValue)
: getOptionValue(options[0]);
const [countryCode, setCountryCode] = useState(defaultCode);
const [localNumber, setLocalNumber] = useState('');
const handleCodeChange = (code: string) => {
setCountryCode(code);
onInputChange(ref, localNumber ? `${code}${localNumber}` : '');
};
const handleNumberChange = (num: string) => {
const digits = num.replace(/\D/g, '');
setLocalNumber(digits);
onInputChange(ref, digits ? `${countryCode}${digits}` : '');
};
const isSingleCode = options.length === 1;
return (
<FormControl required={component.required}>
<FormLabel htmlFor={ref}>{t(resolve(component.label)!)}</FormLabel>
<Box sx={{display: 'flex', gap: 1}}>
{isSingleCode ? (
<StaticCodeDisplay label={getOptionLabel(options[0])} />
) : (
<Select value={countryCode} onChange={...}>
{options.map(opt => <MenuItem .../>)}
</Select>
)}
<TextField
type="tel"
autoComplete="tel-national"
value={localNumber}
onChange={(e) => handleNumberChange(e.target.value)}
...
/>
</Box>
</FormControl>
);
}This isn't meant to be copy-pasted as final code. Look at how Update if (sub.type === 'PHONE_INPUT') {
return <PhoneNumberInputAdapter key={sub.id ?? compIndex} {...fieldProps} />;
}Since Also remove Add defaultValue?: string;Testing
|
Beta Was this translation helpful? Give feedback.


Uh oh!
There was an error while loading. Please reload this page.
-
Problem Statement
Authentication flows often need to support multiple login identifier types — e.g., mobile number, email, national ID — within the same sign-in step. Currently, the Asgardeo UI SDK's flow graph model has no way to express this pattern: the server cannot instruct the SDK to render a type-selector UI alongside a contextual input field in a single flow step.
The existing workaround would require either:
ACTION TRIGGERper login type), introducing noticeable latency on every tab switch, orloginid-typesdesign).Neither is acceptable. The flow graph model should be expressive enough to represent this without special-casing it.
Goals & Non-Goals
Goals
LOGIN_ID_INPUTcomponent type to the embedded flow graph model.prefix + rawInput + postfixbefore submitting to the server. The server receives only the assembled value.default: trueon one login ID type to pre-select it on render.ICONcomponent type) rather than asset-path strings.maxLengthandregexvalidation, consistent with existing form validation inAuthOptionFactory. Validation failures use a generic i18n message — no per-type custom message needed.{{t(...)}}template literal convention for labels and placeholders.Non-Goals
EmbeddedSignInFlowRequest.inputsmap.Acceptance Criteria
New component type registered:
EmbeddedFlowComponentType.LoginIdInput = 'LOGIN_ID_INPUT'exists inpackages/javascript/src/models/v2/embedded-flow-v2.ts.Type definitions complete: A
LoginIdTypeinterface and updatedEmbeddedFlowComponentdiscriminated union are exported from@asgardeo/javascript.Rendered correctly: When
AuthOptionFactoryreceives aLOGIN_ID_INPUTcomponent, it renders:labelas a heading above the grid.LoginIdType.labelas an input field label below the grid.inputType, prefix selector (if applicable), placeholder, maxLength, and validation change to match the selected login ID type.default: trueis pre-selected on mount. If no type is marked default, the first type is selected.Prefix handling:
prefixesis a single string, it is displayed as a static prefix label.prefixesis an array of objects, it is rendered as a dropdown/selector. The user picks one prefix; the selected value is used in assembly.prefixesis absent or empty, no prefix UI is shown.Value assembly: On form submission, the value submitted for the component's
refkey isselectedPrefix.value + rawInput + postfix. If prefix or postfix is absent, those parts are omitted (no trailing/leading empty string concatenation edge cases).Validation:
regexandmaxLengthfrom the active login ID type (with prefix-level overrides applied) are enforced against the raw input. Failures surface via the existingformErrors/fieldErrorsmechanism using a generic i18n fallback message. NovalidationMessagefield onLoginIdTypeis required.Postfix display: The postfix is never shown in the input field. It is appended silently during value assembly on submit.
Icon resolution: The
iconfield on each login ID type is resolved to a lucide-react icon by name, consistent with how the existingICONcomponent type resolves icons. An unrecognized icon name renders no icon without throwing.i18n:
labelandplaceholderfields support{{t(key)}}template literals, resolved by the existing template resolver inAuthOptionFactory.Single-type degenerate case: If only one login ID type is provided, the selector tab row is not rendered — only the input is shown.
Tab switch behavior: Switching login ID type swaps the input instantly (no animation). The raw input value is cleared on type switch; existing validation errors are also cleared.
Accessibility: The type selector uses
role="tablist"/role="tab"(ARIA tab pattern) and is keyboard-navigable (arrow keys between tabs, Enter/Space to select). The input has a properaria-labelderived from the active type's label.Unit tests: Coverage for value assembly logic (prefix + input + postfix permutations), prefix-switch re-validation, and the degenerate single-type case.
Technical Notes
New type:
LoginIdTypeAdd to packages/javascript/src/models/v2/embedded-flow-v2.ts:
Extend
EmbeddedFlowComponentType:Add
loginIdTypesas an optional field onEmbeddedFlowComponent:Value assembly in
AuthOptionFactorypackages/react/src/components/presentation/auth/AuthOptionFactory.tsx adds one new case in
createAuthComponentFromFlow()that renders<LoginIdInput>. All state lives insideuseLoginIdInput—AuthOptionFactoryis only responsible for passingonInputChangeand the component definition through, as it does for all other component types.Value assembly happens at submit time (not on every keystroke) inside
useLoginIdInput:The assembled
finalValueis what gets written intoformValues[ref]before the existing submit path runs. The server derives the login ID type from the assembled value. No additional type metadata field is submitted.Icon resolution
Re-use the existing icon resolution map used by
EmbeddedFlowComponentType.Icon. Theiconfield onLoginIdTypefollows the same lookup — no new resolution mechanism needed.Prefix selector component
PHONE_INPUTis declared inEmbeddedFlowComponentTypebut has no React renderer yet. Rather than waiting for it, the prefix selector should be implemented as a standalonePrefixSelectorcomponent insideLoginIdInput, so thatPHONE_INPUT's future renderer can reuse it without rebuilding the pattern.PrefixSelectorshould be a custom styled dropdown (not a native<select>) to stay consistent with the SDK's component library.Packages touched
@asgardeo/javascriptLoginIdType,LoginIdPrefix; new enum valueLoginIdInput@asgardeo/reactLoginIdInputcomponent; new case inAuthOptionFactory; value assembly logic@asgardeo/i18nerrors.invalidFormat); add it if absentExample flow graph payload
{ "type": "LOGIN_ID_INPUT", "id": "login-id-field", "ref": "username", "label": "{{t(loginId.selector.label)}}", "loginIdTypes": [ { "id": "mobile", "icon": "Smartphone", "label": "{{t(loginId.mobile.label)}}", "placeholder": "{{t(loginId.mobile.placeholder)}}", "prefixes": [ { "label": "IND", "value": "+91", "maxLength": 10 }, { "label": "KHM", "value": "+855", "maxLength": 9 } ], "postfix": "@phone", "regex": "^[0-9]+$", "default": true }, { "id": "email", "icon": "Mail", "label": "{{t(loginId.email.label)}}", "placeholder": "{{t(loginId.email.placeholder)}}", "maxLength": 254 }, { "id": "nrc", "icon": "IdCard", "label": "{{t(loginId.nrc.label)}}", "placeholder": "{{t(loginId.nrc.placeholder)}}", "postfix": "@NRC" } ] }UX Design
The component is built on the same primitives as
TEXT_INPUT:FormControl,InputLabel, andTextField. There are two distinct labels: the component-levellabel(rendered above the button grid as a section heading) and eachLoginIdType.label(rendered below the grid as the input field label, updating when the active type changes).Layout
3 types → 1 row × 3 columns:
4 types → 2 rows × 2 columns:
InputLabelwithvariant="block"— same asTEXT_INPUT.InputLabelwithvariant="block"and updates when the active type changes.FormControl's helper text slot, identical toTEXT_INPUT.FormControlso spacing, error state propagation, and BEM class structure stay consistent.Tab Row (Login ID Type Selector)
Buttoncomponents withvariant="outline"— each button is separate with its own border, not joined into a button group.loginIdTypes.length:columns = count— all buttons on one row, equal width.columns = 2— buttons fill a 2-column grid, wrapping into as many rows as needed.display: grid; grid-template-columns: repeat(columns, 1fr); gap: theme.vars.spacing.unit. Each button stretches to fill its cell (1fr), so rows are always visually balanced.variant="solid"withcolor="primary"to indicate selection.startIcon, followed by the label text.role="tablist"/role="tab"witharia-selectedon each button.titleattribute.Prefix Selector (
PrefixSelector)TextField, inside the same input container — visually appears as a prefixed segment of the field (matching thestartIconpadding pattern inTextField.styles.ts).prefixesis a single string: renders as a static non-interactive label with the same padding/border as the input, separated by a divider.prefixesis an array: renders as a custom dropdown button (not a native<select>) that opens a listbox above/below via Floating UI — consistent with howSelectprimitive uses Floating UI for positioning.Visual States
Mirrors
TEXT_INPUTstates exactly:theme.vars.colors.bordertheme.vars.colors.error.maintheme.vars.colors.background.disabledThe error state applies to the entire input container (prefix + text field unified boundary), not just the text portion.
Transitions
Consistent with
TextField:border-colorandbox-shadowtransition at0.2s ease.BEM Class Structure
Edge Cases or Gaps
No
default: trueset: First type in the array is selected. If the array is empty, the component renders nothing and logs a warning (consistent with howOU_SELECThandles missingrootOuId).Multiple
default: true: First one wins; subsequentdefault: trueflags are ignored.Single login ID type: Selector tab row is suppressed. Only the input is rendered. The single type's prefix/postfix still apply.
Prefix is empty string
"": Treated the same as absent — no prefix UI, no prefix concatenation.User switches login type mid-input: Raw input is cleared on type switch. This avoids submitting e.g. an email address with a phone postfix appended, and avoids surfacing stale validation errors from the previous type.
maxLengthenforcement with prefix:maxLengthapplies to the raw input only (before prefix/postfix), not the assembled value. This must be explicit in implementation to avoid off-by-one truncation. When the user switches prefix within the same login type, the input is not cleared — instead, validation re-runs immediately against the new prefix'smaxLength(or the outermaxLengthif the new prefix does not define one), surfacing an error if the existing input now exceeds the limit.Regex applied to raw input or assembled value: Regex should validate the raw input (before assembly), since the postfix is a known static string and validating the assembled value would require escaping it into the regex. Document this clearly.
Server sends
LOGIN_ID_INPUTwithloginIdTypes: null: Treat as empty array — render nothing, emit alogger.warn.Framework packages beyond
@asgardeo/react: Vue (@asgardeo/vue) and other framework packages each have their own component rendering layer. This document covers@asgardeo/reactonly. Other frameworks will need equivalent implementations tracked separately.Beta Was this translation helpful? Give feedback.
All reactions