From 23f36af5523d16ff10248e6e02aa08e4962012ac Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:08:01 +0000 Subject: [PATCH 1/5] Render identifiers as text inputs --- .changeset/quiet-identifiers.md | 5 +++++ packages/managed-auth-react/src/session/state.test.ts | 8 ++++---- packages/managed-auth-react/src/session/state.ts | 6 +----- 3 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 .changeset/quiet-identifiers.md diff --git a/.changeset/quiet-identifiers.md b/.changeset/quiet-identifiers.md new file mode 100644 index 0000000..763ab79 --- /dev/null +++ b/.changeset/quiet-identifiers.md @@ -0,0 +1,5 @@ +--- +"@onkernel/managed-auth-react": patch +--- + +Render canonical identifier fields as text inputs so combined username, email, and phone login fields accept every supported identifier format. diff --git a/packages/managed-auth-react/src/session/state.test.ts b/packages/managed-auth-react/src/session/state.test.ts index a05e717..1933890 100644 --- a/packages/managed-auth-react/src/session/state.test.ts +++ b/packages/managed-auth-react/src/session/state.test.ts @@ -202,7 +202,7 @@ describe("normalizeManagedAuthState", () => { id: "field_email", ref: "email", type: "identifier", - label: "Email", + label: "Mobile number, username, or email", reason: "missing", }, { @@ -239,15 +239,15 @@ describe("normalizeManagedAuthState", () => { id: "field_email", ref: "email", name: "field_email", - type: "email", - label: "Email", + type: "text", + label: "Mobile number, username, or email", required: true, }, { id: "field_phone", ref: "phone_number", name: "field_phone", - type: "tel", + type: "text", label: "Phone", required: true, }, diff --git a/packages/managed-auth-react/src/session/state.ts b/packages/managed-auth-react/src/session/state.ts index 19f4e77..b3dcb38 100644 --- a/packages/managed-auth-react/src/session/state.ts +++ b/packages/managed-auth-react/src/session/state.ts @@ -40,12 +40,8 @@ function fieldTypeToDiscoveredType( field: ManagedAuthField, ): DiscoveredField["type"] { switch (field.type) { - case "identifier": { - const ref = field.ref.toLowerCase(); - if (ref.includes("email")) return "email"; - if (ref.includes("phone") || ref.includes("tel")) return "tel"; + case "identifier": return "text"; - } case "totp_code": return "totp"; case "totp_secret": From fca286ed57df11b67f94b7010ad8f1ed459387e3 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:11:41 +0000 Subject: [PATCH 2/5] Apply managed auth field input modes --- .changeset/gentle-input-modes.md | 5 +++ .../src/components/UnifiedAuthForm.test.ts | 34 +++++++++++++++++++ .../src/components/UnifiedAuthForm.tsx | 2 ++ packages/managed-auth-react/src/lib/types.ts | 4 +++ .../src/session/state.test.ts | 6 ++++ .../managed-auth-react/src/session/state.ts | 1 + 6 files changed, 52 insertions(+) create mode 100644 .changeset/gentle-input-modes.md diff --git a/.changeset/gentle-input-modes.md b/.changeset/gentle-input-modes.md new file mode 100644 index 0000000..9a150ee --- /dev/null +++ b/.changeset/gentle-input-modes.md @@ -0,0 +1,5 @@ +--- +"@onkernel/managed-auth-react": patch +--- + +Apply canonical field input modes as virtual keyboard hints without enabling browser format validation. diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts index bae2faa..e47f1a2 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts @@ -20,6 +20,40 @@ describe("getAutocomplete", () => { }); describe("UnifiedAuthForm", () => { + test("applies a keyboard hint without enabling native format validation", () => { + let renderer!: ReturnType; + + act(() => { + renderer = create( + createElement(AppearanceProvider, { + children: createElement(LocalizationProvider, { + children: createElement(UnifiedAuthForm, { + targetDomain: "example.com", + fields: [ + { + name: "field_email", + label: "Email address", + type: "text", + input_mode: "email", + }, + ], + onSubmitFields: () => {}, + onSSOClick: () => {}, + onMFASelect: () => {}, + onSignInOptionSelect: () => {}, + }), + }), + }), + ); + }); + + const input = renderer.root.findByType("input"); + expect(input.props.type).toBe("text"); + expect(input.props.inputMode).toBe("email"); + + act(() => renderer.unmount()); + }); + test("renders an accessible, customizable rejection notice", () => { const fieldName = "password-field"; const rejectedNoticeId = `${fieldName}-rejected-notice`; diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx index 1f23bbd..645a36d 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx @@ -265,6 +265,7 @@ export function UnifiedAuthForm({ id={field.name} name={field.name} type={showPassword[field.name] ? "text" : "password"} + inputMode={field.input_mode} placeholder={field.placeholder} required={field.required} autoComplete={getAutocomplete(field)} @@ -300,6 +301,7 @@ export function UnifiedAuthForm({ id={field.name} name={field.name} type={getInputType(field)} + inputMode={field.input_mode} placeholder={field.placeholder} required={field.required} autoComplete={getAutocomplete(field)} diff --git a/packages/managed-auth-react/src/lib/types.ts b/packages/managed-auth-react/src/lib/types.ts index 01fb154..99e0af5 100644 --- a/packages/managed-auth-react/src/lib/types.ts +++ b/packages/managed-auth-react/src/lib/types.ts @@ -27,12 +27,15 @@ export type MFAType = | "switch" | "other"; +export type InputMode = "text" | "email" | "tel" | "numeric"; + export interface DiscoveredField { id?: string; ref?: string; name: string; label: string; type: "text" | "email" | "password" | "tel" | "code" | "totp"; + input_mode?: InputMode; placeholder?: string; required?: boolean; reason?: "missing" | "rejected"; @@ -72,6 +75,7 @@ export interface ManagedAuthField { | "totp_code" | "totp_secret" | "text"; + input_mode?: InputMode; label?: string; required?: boolean; reason: "missing" | "rejected"; diff --git a/packages/managed-auth-react/src/session/state.test.ts b/packages/managed-auth-react/src/session/state.test.ts index 1933890..9b78fe7 100644 --- a/packages/managed-auth-react/src/session/state.test.ts +++ b/packages/managed-auth-react/src/session/state.test.ts @@ -195,6 +195,7 @@ describe("normalizeManagedAuthState", () => { id: "field_username", ref: "username", type: "identifier", + input_mode: "text", label: "Username", reason: "missing", }, @@ -202,6 +203,7 @@ describe("normalizeManagedAuthState", () => { id: "field_email", ref: "email", type: "identifier", + input_mode: "text", label: "Mobile number, username, or email", reason: "missing", }, @@ -209,6 +211,7 @@ describe("normalizeManagedAuthState", () => { id: "field_phone", ref: "phone_number", type: "identifier", + input_mode: "tel", label: "Phone", reason: "missing", }, @@ -232,6 +235,7 @@ describe("normalizeManagedAuthState", () => { ref: "username", name: "field_username", type: "text", + input_mode: "text", label: "Username", required: true, }, @@ -240,6 +244,7 @@ describe("normalizeManagedAuthState", () => { ref: "email", name: "field_email", type: "text", + input_mode: "text", label: "Mobile number, username, or email", required: true, }, @@ -248,6 +253,7 @@ describe("normalizeManagedAuthState", () => { ref: "phone_number", name: "field_phone", type: "text", + input_mode: "tel", label: "Phone", required: true, }, diff --git a/packages/managed-auth-react/src/session/state.ts b/packages/managed-auth-react/src/session/state.ts index b3dcb38..07840af 100644 --- a/packages/managed-auth-react/src/session/state.ts +++ b/packages/managed-auth-react/src/session/state.ts @@ -65,6 +65,7 @@ function fieldsFromCanonical( ref: field.ref, name: field.id, type: fieldTypeToDiscoveredType(field), + ...(field.input_mode ? { input_mode: field.input_mode } : {}), label: field.label || field.ref, placeholder: legacyField?.placeholder, required: field.required ?? true, From 155a98b842d0d33b68e107a2e94cc33971ffc08c Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:39:57 +0000 Subject: [PATCH 3/5] Export managed auth input modes --- packages/managed-auth-react/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/managed-auth-react/src/index.ts b/packages/managed-auth-react/src/index.ts index 9a62bde..a18e9a8 100644 --- a/packages/managed-auth-react/src/index.ts +++ b/packages/managed-auth-react/src/index.ts @@ -37,6 +37,7 @@ export type { AuthSuccessPayload, AuthErrorPayload, MFAType, + InputMode, DiscoveredField, SSOButton, MFAOption, From 0770c2aad9365d0c06bc0b5d77b4e28c57deddf8 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:01:58 +0000 Subject: [PATCH 4/5] Preserve identifier autofill hints --- .../src/components/UnifiedAuthForm.test.ts | 32 +++++++++++++++++++ .../src/components/UnifiedAuthForm.tsx | 2 ++ 2 files changed, 34 insertions(+) diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts index e47f1a2..38142c4 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts @@ -17,6 +17,37 @@ describe("getAutocomplete", () => { }), ).toBe("username"); }); + + test("uses input modes for dedicated email and telephone autofill", () => { + expect( + getAutocomplete({ + name: "field_email", + label: "Email address", + type: "text", + input_mode: "email", + }), + ).toBe("email"); + expect( + getAutocomplete({ + name: "field_phone", + label: "Phone number", + type: "text", + input_mode: "tel", + }), + ).toBe("tel"); + }); + + test("does not infer email autofill for mixed identifier inputs", () => { + expect( + getAutocomplete({ + ref: "email", + name: "field_identifier", + label: "Mobile number, username, or email", + type: "text", + input_mode: "text", + }), + ).toBeUndefined(); + }); }); describe("UnifiedAuthForm", () => { @@ -50,6 +81,7 @@ describe("UnifiedAuthForm", () => { const input = renderer.root.findByType("input"); expect(input.props.type).toBe("text"); expect(input.props.inputMode).toBe("email"); + expect(input.props.autoComplete).toBe("email"); act(() => renderer.unmount()); }); diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx index 645a36d..ebdd5bd 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx @@ -78,6 +78,8 @@ function getInputType(field: DiscoveredField): string { export function getAutocomplete(field: DiscoveredField): string | undefined { const identity = (field.ref ?? field.name).toLowerCase(); + if (field.input_mode === "email") return "email"; + if (field.input_mode === "tel") return "tel"; switch (field.type) { case "email": return "email"; From bf2f95e9568d525edec1a2baaa7a837d56a20299 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:06:34 +0000 Subject: [PATCH 5/5] Prioritize semantic autocomplete hints --- .../src/components/UnifiedAuthForm.test.ts | 11 +++++++++++ .../src/components/UnifiedAuthForm.tsx | 11 +++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts index 38142c4..d6c576c 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts @@ -37,6 +37,17 @@ describe("getAutocomplete", () => { ).toBe("tel"); }); + test("keeps one-time-code autocomplete ahead of keyboard hints", () => { + expect( + getAutocomplete({ + name: "field_code", + label: "Verification code", + type: "code", + input_mode: "tel", + }), + ).toBe("one-time-code"); + }); + test("does not infer email autofill for mixed identifier inputs", () => { expect( getAutocomplete({ diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx index ebdd5bd..60103dc 100644 --- a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx +++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx @@ -78,8 +78,6 @@ function getInputType(field: DiscoveredField): string { export function getAutocomplete(field: DiscoveredField): string | undefined { const identity = (field.ref ?? field.name).toLowerCase(); - if (field.input_mode === "email") return "email"; - if (field.input_mode === "tel") return "tel"; switch (field.type) { case "email": return "email"; @@ -90,11 +88,12 @@ export function getAutocomplete(field: DiscoveredField): string | undefined { case "code": case "totp": return "one-time-code"; - default: - if (identity.includes("user") || identity.includes("identifier")) - return "username"; - return undefined; } + if (field.input_mode === "email") return "email"; + if (field.input_mode === "tel") return "tel"; + if (identity.includes("user") || identity.includes("identifier")) + return "username"; + return undefined; } function getDescriptionIds(field: DiscoveredField): string | undefined {