frus_widgets/autofill.rs
1//! What a field is **for**, so that the platform can fill it in.
2//!
3//! [`KeyboardType`](crate::KeyboardType) says which keys a field wants; it never says
4//! what the field holds. That second thing is what a password manager and the platform's
5//! autofill service read, and without it nothing happens — not loudly, just never: a
6//! saved password is not offered on a sign-in screen, a code arriving by SMS is not
7//! offered above the keyboard, an address is typed out by hand, and a password just
8//! created is never saved, so the next sign-in fails.
9//!
10//! ```ignore
11//! AutofillGroup::new(
12//! Flex::column()
13//! .child(TextField::new(&app.email).autofill([AutofillHint::Username, AutofillHint::Email]))
14//! .child(TextField::new(&app.password).autofill([AutofillHint::NewPassword])),
15//! )
16//! ```
17//!
18//! # A hint is a list, and a form is a group
19//!
20//! A field can legitimately be more than one thing — a sign-in box that takes an email
21//! address is both the username and the email — so a field carries a **list** of hints,
22//! in the order of how well each describes it.
23//!
24//! And a credential is a **pair**. A service that is shown a password with no username
25//! beside it has nothing to save under, so the fields of one form are declared together:
26//! [`AutofillGroup`] is what says which fields those are. A field outside any group
27//! stands alone, which is right for a lone search box and wrong for a sign-in screen.
28//!
29//! # The names are Android's, and the tests are not
30//!
31//! Each hint carries the string Android's autofill service expects
32//! ([`AutofillHint::android`]), and that mapping lives here rather than in the platform
33//! layer — the same reasoning as [`crate::ime`]: a mapping only exercised on a device is
34//! a mapping nobody checks, and the bridge's dex is checked in, so the Java side must
35//! never have to change to learn a hint. It is handed strings and sets them on the
36//! structure.
37//!
38//! The names are rarely the obvious ones — an email address is `emailAddress`, a person's
39//! name is `personName`, a one-time code is `smsOTPCode` — which is exactly why a table
40//! nobody can read off the constant's own name is worth writing down once.
41
42use crate::widget::Widget;
43
44/// What a field is for: what an autofill service should put in it, and what it should
45/// learn from it when the form is submitted.
46///
47/// The catalogue is the useful part of the platform's, not all of it: what a sign-in, a
48/// sign-up, an address and a card payment need. A hint the list does not carry is a hint
49/// to add here, with its platform name and a test.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub enum AutofillHint {
52 /// The account's name — an email address often plays this part as well.
53 Username,
54 /// A username being **created**, so that the service offers to save it rather than
55 /// to fill it.
56 NewUsername,
57 /// An existing password, to be filled.
58 Password,
59 /// A password being **created**: never filled from what is saved, and offered for
60 /// saving once the form is submitted.
61 NewPassword,
62 /// A one-time code, usually arriving by SMS. The platform offers it above the
63 /// keyboard, which is what saves the reader leaving the application to go and read it.
64 OneTimeCode,
65 /// An email address.
66 Email,
67 /// A person's whole name.
68 Name,
69 /// The given (first) name alone.
70 GivenName,
71 /// The family (last) name alone.
72 FamilyName,
73 /// A telephone number.
74 TelephoneNumber,
75 /// The street part of an address — the lines above the town.
76 StreetAddress,
77 /// A whole postal address in one field.
78 PostalAddress,
79 /// The postal code.
80 PostalCode,
81 /// The town or city.
82 AddressCity,
83 /// The state, region or county.
84 AddressState,
85 /// The country.
86 CountryName,
87 /// A payment card's number.
88 CreditCardNumber,
89 /// Its expiry date, as one field.
90 CreditCardExpirationDate,
91 /// Its security code (CVC).
92 CreditCardSecurityCode,
93}
94
95impl AutofillHint {
96 /// The string Android's autofill services expect for this hint.
97 ///
98 /// Several are not what the hint is called: `personName`, `emailAddress`,
99 /// `phoneNumber`, `addressLocality` for a town, `addressRegion` for a state,
100 /// `addressCountry` for a country, `smsOTPCode` for a one-time code.
101 pub const fn android(self) -> &'static str {
102 match self {
103 Self::Username => "username",
104 Self::NewUsername => "newUsername",
105 Self::Password => "password",
106 Self::NewPassword => "newPassword",
107 Self::OneTimeCode => "smsOTPCode",
108 Self::Email => "emailAddress",
109 Self::Name => "personName",
110 Self::GivenName => "personGivenName",
111 Self::FamilyName => "personFamilyName",
112 Self::TelephoneNumber => "phoneNumber",
113 Self::StreetAddress => "streetAddress",
114 Self::PostalAddress => "postalAddress",
115 Self::PostalCode => "postalCode",
116 Self::AddressCity => "addressLocality",
117 Self::AddressState => "addressRegion",
118 Self::CountryName => "addressCountry",
119 Self::CreditCardNumber => "creditCardNumber",
120 Self::CreditCardExpirationDate => "creditCardExpirationDate",
121 Self::CreditCardSecurityCode => "creditCardSecurityCode",
122 }
123 }
124
125 /// Is what this field holds a **secret**, so that it must never be shown in a
126 /// screenshot of the structure, nor learned by a keyboard?
127 ///
128 /// The field's [`KeyboardType`](crate::KeyboardType) already stops the keyboard
129 /// learning it; this is the same question asked of the *hint*, for the paths that
130 /// only ever see one — reporting a value to the autofill service, above all.
131 pub const fn is_secret(self) -> bool {
132 matches!(self, Self::Password | Self::NewPassword | Self::OneTimeCode)
133 }
134}
135
136/// The fields of **one form**, declared together so that the platform can fill them and
137/// save them as a set.
138///
139/// A transparent wrapper: it changes nothing about how its subtree is laid out or
140/// painted. What it does is answer, for every field inside it, *which form is this part
141/// of* — and a password with no username beside it is a credential a service cannot save.
142///
143/// It is the reference's `AutofillGroup`.
144pub struct AutofillGroup<Msg = crate::callback::Callback> {
145 inner: Box<dyn Widget<Msg>>,
146}
147
148impl<Msg> AutofillGroup<Msg> {
149 /// Declares `inner`'s fields to be one form.
150 pub fn new(inner: impl Widget<Msg> + 'static) -> Self {
151 Self {
152 inner: Box::new(inner),
153 }
154 }
155
156 /// A group is not a box: its child's own, unchanged.
157 fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
158 base
159 }
160}
161
162crate::transparent::forward_transparent!(AutofillGroup {
163 /// The one thing it does: name itself as the form its fields belong to.
164 fn autofill_group(&self) -> bool {
165 true
166 }
167 /// Forwarded: the area's bar is the area's, whatever wraps it.
168 fn area_toolbar(
169 &self,
170 context: crate::ToolbarContext,
171 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
172 self.inner.area_toolbar(context)
173 }
174
175 /// A form is not an identity: the child's key, if it has one.
176 fn key(&self) -> Option<u64> {
177 self.inner.key()
178 }
179
180 /// Forwarded, as for every transparent wrapper: a form is not a place, not a theme,
181 /// and not a surface.
182 fn positioned(&self) -> Option<crate::positioned::Positioning> {
183 self.inner.positioned()
184 }
185 fn theme_override(
186 &self,
187 inherited: &crate::theme::Theme,
188 ) -> Option<Box<crate::theme::Theme>> {
189 self.inner.theme_override(inherited)
190 }
191 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
192 self.inner.media_override(inherited)
193 }
194 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
195 self.inner.scaffold_override()
196 }
197 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
198 self.inner.scroll_behavior_override()
199 }
200});
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::{Container, TextField};
206
207 /// Every hint this crate offers, for the tests that must cover all of them.
208 const EVERY: [AutofillHint; 19] = [
209 AutofillHint::Username,
210 AutofillHint::NewUsername,
211 AutofillHint::Password,
212 AutofillHint::NewPassword,
213 AutofillHint::OneTimeCode,
214 AutofillHint::Email,
215 AutofillHint::Name,
216 AutofillHint::GivenName,
217 AutofillHint::FamilyName,
218 AutofillHint::TelephoneNumber,
219 AutofillHint::StreetAddress,
220 AutofillHint::PostalAddress,
221 AutofillHint::PostalCode,
222 AutofillHint::AddressCity,
223 AutofillHint::AddressState,
224 AutofillHint::CountryName,
225 AutofillHint::CreditCardNumber,
226 AutofillHint::CreditCardExpirationDate,
227 AutofillHint::CreditCardSecurityCode,
228 ];
229
230 /// A hint whose platform name is wrong is a field the service silently declines to
231 /// fill, so each one is written down and checked. The names that are **not** the
232 /// hint's own are the ones worth the test: a service reading `email` or `name` fills
233 /// nothing at all.
234 #[test]
235 fn each_hint_carries_the_platforms_own_name() {
236 assert_eq!(AutofillHint::Email.android(), "emailAddress");
237 assert_eq!(AutofillHint::Name.android(), "personName");
238 assert_eq!(AutofillHint::GivenName.android(), "personGivenName");
239 assert_eq!(AutofillHint::FamilyName.android(), "personFamilyName");
240 assert_eq!(AutofillHint::TelephoneNumber.android(), "phoneNumber");
241 assert_eq!(AutofillHint::OneTimeCode.android(), "smsOTPCode");
242 assert_eq!(AutofillHint::AddressCity.android(), "addressLocality");
243 assert_eq!(AutofillHint::AddressState.android(), "addressRegion");
244 assert_eq!(AutofillHint::CountryName.android(), "addressCountry");
245 // And the ones that are their own name are still named, so that adding a hint
246 // without a mapping cannot pass by defaulting to something plausible.
247 assert_eq!(AutofillHint::Username.android(), "username");
248 assert_eq!(AutofillHint::NewPassword.android(), "newPassword");
249 }
250
251 /// No two hints may share a name — two fields carrying the same one is a form the
252 /// service fills twice with the same value.
253 #[test]
254 fn no_two_hints_share_a_name() {
255 let mut seen: Vec<&str> = EVERY.iter().map(|h| h.android()).collect();
256 let count = seen.len();
257 seen.sort_unstable();
258 seen.dedup();
259 assert_eq!(seen.len(), count, "a name is used twice");
260 assert!(EVERY.iter().all(|h| !h.android().is_empty()));
261 }
262
263 /// What must never be reported in the clear, nor learned: the passwords and the
264 /// one-time code. An email address is not a secret, and treating it as one would
265 /// stop a service ever offering it.
266 #[test]
267 fn the_secrets_are_the_passwords_and_the_code() {
268 for hint in [
269 AutofillHint::Password,
270 AutofillHint::NewPassword,
271 AutofillHint::OneTimeCode,
272 ] {
273 assert!(hint.is_secret(), "{hint:?}");
274 }
275 for hint in [
276 AutofillHint::Username,
277 AutofillHint::Email,
278 AutofillHint::CreditCardNumber,
279 ] {
280 assert!(!hint.is_secret(), "{hint:?}");
281 }
282 }
283
284 /// A group says what it is and delegates the rest; the fields inside keep their own
285 /// hints, which is what the shell reads.
286 #[test]
287 fn a_group_names_itself_and_changes_nothing_else() {
288 let field = TextField::<()>::new("someone@example.com")
289 .autofill([AutofillHint::Username, AutofillHint::Email]);
290 let group = AutofillGroup::new(Container::<()>::new().child(field));
291 assert!(Widget::<()>::autofill_group(&group));
292 assert_eq!(Widget::<()>::children(&group).len(), 1);
293 // And nothing that is not a group answers that it is.
294 assert!(!Widget::<()>::autofill_group(&Container::<()>::new()));
295 }
296
297 /// A wrapper that fuses with its field shares its identity, so it is the wrapper the
298 /// shell asks what the field is for — and one that forgot to pass the question on
299 /// would take the field out of every form without a word. `Responsive` is written by
300 /// hand, and has already cost four silent bugs of exactly this kind.
301 #[test]
302 fn the_wrappers_that_fuse_with_a_field_pass_its_hints_on() {
303 let field = || TextField::<()>::new("").autofill([AutofillHint::OneTimeCode]);
304 let expected = [AutofillHint::OneTimeCode];
305 let keyed = crate::Keyed::new(1, field());
306 let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
307 let boxed: Box<dyn Widget<()>> = Box::new(field());
308 assert_eq!(Widget::<()>::autofill_hints(&keyed), &expected);
309 assert_eq!(Widget::<()>::autofill_hints(&responsive), &expected);
310 assert_eq!(boxed.autofill_hints(), &expected);
311 // And a group behind a selector, a key or a box is still a group.
312 let grouped = crate::Responsive::new(crate::SizeClass::Compact)
313 .compact(AutofillGroup::new(Container::<()>::new()));
314 assert!(Widget::<()>::autofill_group(&grouped));
315 let keyed = crate::Keyed::new(2, AutofillGroup::new(Container::<()>::new()));
316 assert!(Widget::<()>::autofill_group(&keyed));
317 let boxed: Box<dyn Widget<()>> = Box::new(AutofillGroup::new(Container::<()>::new()));
318 assert!(boxed.autofill_group());
319 }
320
321 /// A field carries its hints in the order it was given them: the first is what the
322 /// field mostly is, and a service that understands only one reads that one.
323 #[test]
324 fn a_field_keeps_the_hints_it_was_given_in_order() {
325 let field =
326 TextField::<()>::new("").autofill([AutofillHint::Username, AutofillHint::Email]);
327 assert_eq!(
328 Widget::<()>::autofill_hints(&field),
329 &[AutofillHint::Username, AutofillHint::Email]
330 );
331 // A field nobody described carries none, and is not part of any structure.
332 assert!(Widget::<()>::autofill_hints(&TextField::<()>::new("")).is_empty());
333 }
334}