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    /// 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}