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
168 /// A form is not an identity: the child's key, if it has one.
169 fn key(&self) -> Option<u64> {
170 self.inner.key()
171 }
172
173 /// Forwarded, as for every transparent wrapper: a form is not a place, not a theme,
174 /// and not a surface.
175 fn positioned(&self) -> Option<crate::positioned::Positioning> {
176 self.inner.positioned()
177 }
178 fn theme_override(
179 &self,
180 inherited: &crate::theme::Theme,
181 ) -> Option<Box<crate::theme::Theme>> {
182 self.inner.theme_override(inherited)
183 }
184 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
185 self.inner.media_override(inherited)
186 }
187 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
188 self.inner.scaffold_override()
189 }
190});
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::{Container, TextField};
196
197 /// Every hint this crate offers, for the tests that must cover all of them.
198 const EVERY: [AutofillHint; 19] = [
199 AutofillHint::Username,
200 AutofillHint::NewUsername,
201 AutofillHint::Password,
202 AutofillHint::NewPassword,
203 AutofillHint::OneTimeCode,
204 AutofillHint::Email,
205 AutofillHint::Name,
206 AutofillHint::GivenName,
207 AutofillHint::FamilyName,
208 AutofillHint::TelephoneNumber,
209 AutofillHint::StreetAddress,
210 AutofillHint::PostalAddress,
211 AutofillHint::PostalCode,
212 AutofillHint::AddressCity,
213 AutofillHint::AddressState,
214 AutofillHint::CountryName,
215 AutofillHint::CreditCardNumber,
216 AutofillHint::CreditCardExpirationDate,
217 AutofillHint::CreditCardSecurityCode,
218 ];
219
220 /// A hint whose platform name is wrong is a field the service silently declines to
221 /// fill, so each one is written down and checked. The names that are **not** the
222 /// hint's own are the ones worth the test: a service reading `email` or `name` fills
223 /// nothing at all.
224 #[test]
225 fn each_hint_carries_the_platforms_own_name() {
226 assert_eq!(AutofillHint::Email.android(), "emailAddress");
227 assert_eq!(AutofillHint::Name.android(), "personName");
228 assert_eq!(AutofillHint::GivenName.android(), "personGivenName");
229 assert_eq!(AutofillHint::FamilyName.android(), "personFamilyName");
230 assert_eq!(AutofillHint::TelephoneNumber.android(), "phoneNumber");
231 assert_eq!(AutofillHint::OneTimeCode.android(), "smsOTPCode");
232 assert_eq!(AutofillHint::AddressCity.android(), "addressLocality");
233 assert_eq!(AutofillHint::AddressState.android(), "addressRegion");
234 assert_eq!(AutofillHint::CountryName.android(), "addressCountry");
235 // And the ones that are their own name are still named, so that adding a hint
236 // without a mapping cannot pass by defaulting to something plausible.
237 assert_eq!(AutofillHint::Username.android(), "username");
238 assert_eq!(AutofillHint::NewPassword.android(), "newPassword");
239 }
240
241 /// No two hints may share a name — two fields carrying the same one is a form the
242 /// service fills twice with the same value.
243 #[test]
244 fn no_two_hints_share_a_name() {
245 let mut seen: Vec<&str> = EVERY.iter().map(|h| h.android()).collect();
246 let count = seen.len();
247 seen.sort_unstable();
248 seen.dedup();
249 assert_eq!(seen.len(), count, "a name is used twice");
250 assert!(EVERY.iter().all(|h| !h.android().is_empty()));
251 }
252
253 /// What must never be reported in the clear, nor learned: the passwords and the
254 /// one-time code. An email address is not a secret, and treating it as one would
255 /// stop a service ever offering it.
256 #[test]
257 fn the_secrets_are_the_passwords_and_the_code() {
258 for hint in [
259 AutofillHint::Password,
260 AutofillHint::NewPassword,
261 AutofillHint::OneTimeCode,
262 ] {
263 assert!(hint.is_secret(), "{hint:?}");
264 }
265 for hint in [
266 AutofillHint::Username,
267 AutofillHint::Email,
268 AutofillHint::CreditCardNumber,
269 ] {
270 assert!(!hint.is_secret(), "{hint:?}");
271 }
272 }
273
274 /// A group says what it is and delegates the rest; the fields inside keep their own
275 /// hints, which is what the shell reads.
276 #[test]
277 fn a_group_names_itself_and_changes_nothing_else() {
278 let field = TextField::<()>::new("someone@example.com")
279 .autofill([AutofillHint::Username, AutofillHint::Email]);
280 let group = AutofillGroup::new(Container::<()>::new().child(field));
281 assert!(Widget::<()>::autofill_group(&group));
282 assert_eq!(Widget::<()>::children(&group).len(), 1);
283 // And nothing that is not a group answers that it is.
284 assert!(!Widget::<()>::autofill_group(&Container::<()>::new()));
285 }
286
287 /// A wrapper that fuses with its field shares its identity, so it is the wrapper the
288 /// shell asks what the field is for — and one that forgot to pass the question on
289 /// would take the field out of every form without a word. `Responsive` is written by
290 /// hand, and has already cost four silent bugs of exactly this kind.
291 #[test]
292 fn the_wrappers_that_fuse_with_a_field_pass_its_hints_on() {
293 let field = || TextField::<()>::new("").autofill([AutofillHint::OneTimeCode]);
294 let expected = [AutofillHint::OneTimeCode];
295 let keyed = crate::Keyed::new(1, field());
296 let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
297 let boxed: Box<dyn Widget<()>> = Box::new(field());
298 assert_eq!(Widget::<()>::autofill_hints(&keyed), &expected);
299 assert_eq!(Widget::<()>::autofill_hints(&responsive), &expected);
300 assert_eq!(boxed.autofill_hints(), &expected);
301 // And a group behind a selector, a key or a box is still a group.
302 let grouped = crate::Responsive::new(crate::SizeClass::Compact)
303 .compact(AutofillGroup::new(Container::<()>::new()));
304 assert!(Widget::<()>::autofill_group(&grouped));
305 let keyed = crate::Keyed::new(2, AutofillGroup::new(Container::<()>::new()));
306 assert!(Widget::<()>::autofill_group(&keyed));
307 let boxed: Box<dyn Widget<()>> = Box::new(AutofillGroup::new(Container::<()>::new()));
308 assert!(boxed.autofill_group());
309 }
310
311 /// A field carries its hints in the order it was given them: the first is what the
312 /// field mostly is, and a service that understands only one reads that one.
313 #[test]
314 fn a_field_keeps_the_hints_it_was_given_in_order() {
315 let field =
316 TextField::<()>::new("").autofill([AutofillHint::Username, AutofillHint::Email]);
317 assert_eq!(
318 Widget::<()>::autofill_hints(&field),
319 &[AutofillHint::Username, AutofillHint::Email]
320 );
321 // A field nobody described carries none, and is not part of any structure.
322 assert!(Widget::<()>::autofill_hints(&TextField::<()>::new("")).is_empty());
323 }
324}