Skip to main content

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}