@@ -23,7 +23,8 @@ const RESCAN_DEBOUNCE_MS = 250
2323
2424interface DetectedForm {
2525 username : HTMLInputElement | null
26- password : HTMLInputElement
26+ /** Absent on an identifier-first step, which asks for the email alone. */
27+ password : HTMLInputElement | null
2728}
2829
2930/** Held only in this isolated world; never serialized to main or the page. */
@@ -74,15 +75,46 @@ function findUsernameField(password: HTMLInputElement): HTMLInputElement | null
7475 return preceding . at ( - 1 ) ?? candidates [ 0 ] ?? null
7576}
7677
77- function reportFormState ( ) : void {
78+ /**
79+ * The identifier field of a sign-in step that has no password field yet.
80+ *
81+ * Two-step sign-in — email, Continue, then the password on the next screen —
82+ * is now the norm at Google, Okta, and most workplace tools. Requiring a
83+ * password field would mean the key icon never appears on the step where the
84+ * user actually needs it. Only the page's own declaration counts here: an
85+ * `autocomplete` token naming a username or email, or an email input. That is
86+ * narrow enough to leave newsletter boxes and search fields alone.
87+ */
88+ function findIdentifierField ( ) : HTMLInputElement | null {
89+ for ( const field of document . querySelectorAll ( 'input' ) ) {
90+ if ( ! isFillable ( field ) ) continue
91+ const hint = String ( field . getAttribute ( 'autocomplete' ) || '' ) . toLowerCase ( )
92+ if ( hint === 'username' || hint === 'email' ) return field
93+ if ( String ( field . type || '' ) . toLowerCase ( ) === 'email' ) return field
94+ }
95+ return null
96+ }
97+
98+ function detectForm ( ) : DetectedForm | null {
7899 const password = findPasswordField ( )
79- detected = password ? { password, username : findUsernameField ( password ) } : null
100+ if ( password ) return { password, username : findUsernameField ( password ) }
101+ const username = findIdentifierField ( )
102+ return username ? { password : null , username } : null
103+ }
104+
105+ function reportFormState ( ) : void {
106+ detected = detectForm ( )
80107
81108 const origin = window . location . origin
82- const fingerprint = `${ origin } |${ detected !== null } `
109+ const hasPasswordField = detected ?. password != null
110+ const fingerprint = `${ origin } |${ detected !== null } |${ hasPasswordField } `
83111 if ( fingerprint === lastReported ) return
84112 lastReported = fingerprint
85- ipcRenderer . send ( FORM_STATE_CHANNEL , { origin, hasLoginForm : detected !== null } )
113+ ipcRenderer . send ( FORM_STATE_CHANNEL , {
114+ origin,
115+ hasLoginForm : detected !== null ,
116+ hasPasswordField,
117+ } )
86118}
87119
88120function scheduleRescan ( ) : void {
@@ -111,18 +143,21 @@ function setFieldValue(field: HTMLInputElement, value: string): void {
111143
112144ipcRenderer . on (
113145 FILL_CHANNEL ,
114- ( _event , payload : { origin : string ; username : string ; password : string } ) => {
146+ ( _event , payload : { origin : string ; username : string ; password ? : string } ) => {
115147 // Last line of defence against a navigation between the user's choice and
116148 // this message arriving: the main process binds the fill to an origin, and
117149 // the live document has to still agree.
118150 if ( ! payload || payload . origin !== window . location . origin || ! detected ) return
119151 if ( detected . username && payload . username ) {
120152 setFieldValue ( detected . username , payload . username )
121153 }
122- setFieldValue ( detected . password , payload . password )
154+ if ( detected . password && payload . password ) {
155+ setFieldValue ( detected . password , payload . password )
156+ }
123157 // Never submitted. Autofill and submission stay separate so the user can
124- // confirm the site and the account before anything is sent.
125- detected . password . focus ( )
158+ // confirm the site and the account before anything is sent. Focus lands on
159+ // whichever field the user still has to deal with.
160+ ; ( detected . password ?? detected . username ) ?. focus ( )
126161 }
127162)
128163
0 commit comments