Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/gentle-input-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@onkernel/managed-auth-react": patch
---

Apply canonical field input modes as virtual keyboard hints without enabling browser format validation.
5 changes: 5 additions & 0 deletions .changeset/quiet-identifiers.md
Original file line number Diff line number Diff line change
@@ -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.
77 changes: 77 additions & 0 deletions packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,86 @@ 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("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({
ref: "email",
name: "field_identifier",
label: "Mobile number, username, or email",
type: "text",
input_mode: "text",
}),
).toBeUndefined();
});
});

describe("UnifiedAuthForm", () => {
test("applies a keyboard hint without enabling native format validation", () => {
let renderer!: ReturnType<typeof create>;

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");
expect(input.props.autoComplete).toBe("email");

act(() => renderer.unmount());
});

test("renders an accessible, customizable rejection notice", () => {
const fieldName = "password-field";
const rejectedNoticeId = `${fieldName}-rejected-notice`;
Expand Down
11 changes: 7 additions & 4 deletions packages/managed-auth-react/src/components/UnifiedAuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,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 {
Expand Down Expand Up @@ -265,6 +266,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)}
Expand Down Expand Up @@ -300,6 +302,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)}
Expand Down
1 change: 1 addition & 0 deletions packages/managed-auth-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type {
AuthSuccessPayload,
AuthErrorPayload,
MFAType,
InputMode,
DiscoveredField,
SSOButton,
MFAOption,
Expand Down
4 changes: 4 additions & 0 deletions packages/managed-auth-react/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -72,6 +75,7 @@ export interface ManagedAuthField {
| "totp_code"
| "totp_secret"
| "text";
input_mode?: InputMode;
label?: string;
required?: boolean;
reason: "missing" | "rejected";
Expand Down
14 changes: 10 additions & 4 deletions packages/managed-auth-react/src/session/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,20 +195,23 @@ describe("normalizeManagedAuthState", () => {
id: "field_username",
ref: "username",
type: "identifier",
input_mode: "text",
label: "Username",
reason: "missing",
},
{
id: "field_email",
ref: "email",
type: "identifier",
label: "Email",
input_mode: "text",
label: "Mobile number, username, or email",
reason: "missing",
},
{
id: "field_phone",
ref: "phone_number",
type: "identifier",
input_mode: "tel",
label: "Phone",
reason: "missing",
},
Expand All @@ -232,22 +235,25 @@ describe("normalizeManagedAuthState", () => {
ref: "username",
name: "field_username",
type: "text",
input_mode: "text",
label: "Username",
required: true,
},
{
id: "field_email",
ref: "email",
name: "field_email",
type: "email",
label: "Email",
type: "text",
input_mode: "text",
label: "Mobile number, username, or email",
required: true,
},
{
id: "field_phone",
ref: "phone_number",
name: "field_phone",
type: "tel",
type: "text",
input_mode: "tel",
label: "Phone",
required: true,
},
Expand Down
7 changes: 2 additions & 5 deletions packages/managed-auth-react/src/session/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,8 @@ function fieldTypeToDiscoveredType(
field: ManagedAuthField,
): DiscoveredField["type"] {
switch (field.type) {
case "identifier": {
const ref = field.ref.toLowerCase();
Comment thread
cursor[bot] marked this conversation as resolved.
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":
Expand All @@ -69,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,
Expand Down
Loading