Skip to content

Declare a text input's content type for AutoFill - #112

Open
nrobates wants to merge 1 commit into
NativePHP:mainfrom
nrobates:text-input-content-type
Open

nrobates wants to merge 1 commit into
NativePHP:mainfrom
nrobates:text-input-content-type

Conversation

@nrobates

Copy link
Copy Markdown

Closes NativePHP/mobile-air#422 once merged. It lives here rather than in mobile-air because the text input renderer is a plugin component, so the cross-repo reference won't close the issue automatically; it will need closing by hand.

#422 reported two things, and with this PR both are dealt with:

  1. The bug: a secure field filled by AutoFill as the partner of another field never reached @change. This no longer reproduces on stock 0.6.0 (verified on an iPhone; see Testing), so it was fixed somewhere between 0.4.0 and 0.6.0.
  2. The missing API: there was no way to tell the platform what a field holds, which was #422's first suggested fix. This PR adds it.

This is a feature, not the fix for #422. The bug #422 reports (a secure field filled by AutoFill as the partner of another field never reaching @change) no longer reproduces on stock 0.6.0; see Testing. What #422 also asked for, and what nothing in mobile-ui offers yet, is a way to tell the platform what a field holds. That's this PR.

What it adds

A content-type attribute on every text input variant (outlined-text-input, filled-text-input, bare-text-input), also accepted as contentType and as HTML's autocomplete, plus a fluent ->contentType().

Value iOS textContentType Android ContentType
username .username Username
email .emailAddress EmailAddress
password (alias current-password) .password Password
new-password .newPassword NewPassword
one-time-code .oneTimeCode SmsOtpCode

Values are HTML autocomplete tokens. camelCase and snake_case are normalized (newPassword, new_password → new-password), and email-address is an alias for email. Unknown values pass through and are ignored natively, the same policy as keyboard and autocapitalize.

<outlined-text-input label="Email" keyboard="email" content-type="username" @change="setEmail" />
<outlined-text-input label="Password" secure content-type="password" @change="setPassword" />
<outlined-text-input label="New password" secure content-type="new-password" @change="setPassword" />
<bare-text-input keyboard="number" content-type="one-time-code" @change="setCode" />

Why

Neither renderer sets a content type, so there's no way to tell the OS which field is which:

  • a login form has no declared credential pair;
  • a sign-up field gets no strong-password suggestion, because that needs new-password;
  • a code field gets no SMS one-time-code autofill, because that needs one-time-code.

Unset means unchanged

Nothing is inferred when the attribute is absent, so every existing field behaves exactly as it did.

  • PHP: the prop is omitted entirely when unset. It is never serialized as an empty string, so neither resolver sees a choice that wasn't made.
  • iOS: the modifier is only applied when a type is set. It deliberately doesn't call .textContentType(nil) because SwiftUI may assign its own type to a SecureField, and an explicit nil could change existing password fields.
  • Android: Modifier.semantics { contentType = … } is only added when set, so Compose's own keyboard-derived type is untouched otherwise.

Android notes, from the Compose 1.10.0 source (BOM 2025.12.00)

  • Value-based fields support autofill. CoreTextFieldSemanticsModifier handles onFillData and routes a fill through the normal value-change path, so it reaches onValueChange.
  • An explicit type wins over the derived one. Compose already derives a type from the keyboard type (Email → EmailAddress, Password → Password, Phone → PhoneNumber). The author's modifier sits at the head of the chain, and semantics are applied tail to head, so the author's value is written last. keyboard="email" with content-type="username" therefore works.
  • secure alone gets no derived type. Masking is a visual transformation, not a keyboard type, so on Android this prop is the only way to declare such a field a password.

Not in this PR

Testing

  • tests/BaseTextInputContentTypeTest.php: 27 tests covering all three variants and all three attribute spellings, normalization, aliases, omission when unset or empty, pass-through of unknown values, and coexistence with keyboard and secure.
  • Full suite green (289 passed), Pint clean.
  • iOS, physical iPhone 17 Pro Max (iOS 26), 1Password, in an app using these attributes:
    • A login filled from the email field reached the password's @change and signed in, and a one-time code filled a one-time-code field.
    • Control: the same app on stock 0.6.0, with no content-type, also signed in. So #422's partner-fill bug is already gone in 0.6.0, and this PR doesn't fix it.
    • The app has no associated domains, so iOS could not suggest the login or code on its own either way. The fill was selected manually from the key icon.
  • Android, emulator (API 37), Google autofill service:
    • Builds and runs.
    • On a login form declaring username and password, Google autofill returned one dataset covering both fields and filled them as a pair, and tracked the pair for saving.
    • The single one-time-code input was offered to the service as fillable.
    • ⚠️ No stock-0.6.0 control was run on Android, and Google also infers from the keyboard type, so I can't claim the pair fill depends on this PR.

Adds a `content-type` attribute (also `contentType`, and HTML's
`autocomplete`) to every text input variant. It maps to
`textContentType` on iOS and the Compose autofill `contentType` on
Android: username, email, password (current-password), new-password
and one-time-code.

Without it iOS has no declared credential pair on a login form, and a
secure field filled as the partner of another field can be written in
UIKit without its `@change` ever firing (NativePHP/mobile-air#422).
Nothing is inferred when the attribute is unset, so existing fields
behave exactly as before on both platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS Password AutoFill never reaches PHP on a secure text input unless the field is focused when it is filled

1 participant