Multiple LOGIN_ID_INPUT Flow Component #2136
anushasunkada
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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