fix(app): keyboards learn nothing of a password, a code or the phrase

The fields for a password, a code and the recovery phrase's words already
turned suggestions and corrections off; keyboards (Gboard, SwiftKey) still
learnt what was typed into them unless told not to. They now are
(enableIMEPersonalizedLearning: false), and the two sheets taking the
whole phrase share one PhraseField.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 14:48:43 +02:00
commit c9904de31d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 106 additions and 31 deletions

View file

@ -90,24 +90,8 @@ class _AccountRecoveryState() extends State<_AccountRecovery> {
}
}
Widget _phraseField(bool enabled, {String? errorText}) {
final l10n = HalkyonAppLocalizations.of(context);
return TextField(
controller: _phrase,
enabled: enabled,
autocorrect: false,
enableSuggestions: false,
minLines: 2,
maxLines: 5,
maxLength: TextLimits.passage,
buildCounter: nearLimitCounter,
decoration: InputDecoration(
labelText: l10n.passwordPhraseField,
hintText: l10n.passwordPhraseFieldHint,
errorText: errorText,
),
);
}
Widget _phraseField(bool enabled, {String? errorText}) =>
PhraseField(controller: _phrase, enabled: enabled, errorText: errorText);
@override
Widget build(BuildContext context) {

View file

@ -266,21 +266,12 @@ class _PasswordSignInState() extends State<_PasswordSignIn> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SheetIntro(l10n.passwordPhraseFieldIntro),
TextField(
PhraseField(
controller: _phrase,
enabled: enabled,
autofocus: true,
autocorrect: false,
enableSuggestions: false,
minLines: 3,
maxLines: 5,
maxLength: TextLimits.passage,
buildCounter: nearLimitCounter,
decoration: InputDecoration(
labelText: l10n.passwordPhraseField,
hintText: l10n.passwordPhraseFieldHint,
errorText: problem,
),
errorText: problem,
),
],
),

View file

@ -52,6 +52,8 @@ class _PasswordFieldState() extends State<PasswordField> {
obscureText: !_shown,
autocorrect: false,
enableSuggestions: false,
// Nothing typed here is the keyboard's to learn.
enableIMEPersonalizedLearning: false,
keyboardType: TextInputType.visiblePassword,
autofillHints: [
if (widget.isNew)
@ -155,6 +157,8 @@ class const CodeField({
autofillHints: const [AutofillHints.oneTimeCode],
autocorrect: false,
enableSuggestions: false,
// Nothing typed here is the keyboard's to learn.
enableIMEPersonalizedLearning: false,
keyboardType: digits ? TextInputType.number : TextInputType.text,
textCapitalization: TextCapitalization.characters,
textInputAction: textInputAction,
@ -357,6 +361,41 @@ class const RecoveryPhraseView({required final String phrase, super.key})
}
}
/// The field a recovery phrase is typed into: its words, whatever spaces or
/// lines are between them, nothing the keyboard suggests, corrects or
/// learns.
class const PhraseField({
required final TextEditingController controller,
final bool enabled = true,
final bool autofocus = false,
final String? errorText,
final int minLines = 2,
super.key,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = HalkyonAppLocalizations.of(context);
return TextField(
controller: controller,
enabled: enabled,
autofocus: autofocus,
autocorrect: false,
enableSuggestions: false,
// Nothing typed here is the keyboard's to learn.
enableIMEPersonalizedLearning: false,
minLines: minLines,
maxLines: 5,
maxLength: TextLimits.passage,
buildCounter: nearLimitCounter,
decoration: InputDecoration(
labelText: l10n.passwordPhraseField,
hintText: l10n.passwordPhraseFieldHint,
errorText: errorText,
),
);
}
}
/// Fields for the words of a recovery phrase at [positions] (from 0).
class const PhraseCheckFields({
required final List<int> positions,
@ -379,6 +418,8 @@ class const PhraseCheckFields({
autofocus: i == 0,
autocorrect: false,
enableSuggestions: false,
// Nothing typed here is the keyboard's to learn.
enableIMEPersonalizedLearning: false,
textInputAction: i == positions.length - 1
? TextInputAction.done
: TextInputAction.next,

View file

@ -38,8 +38,7 @@ void main() {
AppScope(
dependencies: TestAppDependencies(gateway: FakeAuthGateway()),
child: MaterialApp(
localizationsDelegates:
HalkyonAppLocalizations.localizationsDelegates,
localizationsDelegates: testLocalizationsDelegates,
supportedLocales: HalkyonAppLocalizations.supportedLocales,
home: Scaffold(body: SingleChildScrollView(child: view)),
),

View file

@ -0,0 +1,60 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_app/testing.dart';
import 'package:material_ui/material_ui.dart';
void main() {
/// Pumps [field] alone.
Future<void> pump(WidgetTester tester, Widget field) => tester.pumpWidget(
MaterialApp(
localizationsDelegates: testLocalizationsDelegates,
supportedLocales: HalkyonAppLocalizations.supportedLocales,
home: Scaffold(body: SingleChildScrollView(child: field)),
),
);
/// Whether every text field shown keeps what is typed from the keyboard:
/// no suggestions, no corrections, nothing learnt.
void expectNothingLearnt(WidgetTester tester, String what) {
final fields = tester.widgetList<TextField>(find.byType(TextField));
expect(fields, isNotEmpty, reason: what);
for (final field in fields) {
expect(field.enableIMEPersonalizedLearning, isFalse, reason: what);
expect(field.autocorrect, isFalse, reason: what);
expect(field.enableSuggestions, isFalse, reason: what);
}
}
testWidgets('a keyboard learns nothing of a password, a code or the '
'recovery phrase', (tester) async {
final controller = TextEditingController();
addTearDown(controller.dispose);
await pump(tester, PasswordField(controller: controller, label: 'P'));
expectNothingLearnt(tester, 'a password');
await pump(
tester,
CodeField(controller: controller, kind: CodeKind.totp),
);
expectNothingLearnt(tester, 'a code');
await pump(
tester,
CodeField(controller: controller, kind: CodeKind.recovery),
);
expectNothingLearnt(tester, 'a recovery code');
await pump(tester, PhraseField(controller: controller));
expectNothingLearnt(tester, 'the phrase');
await pump(
tester,
PhraseCheckFields(
positions: const [0, 5],
controllers: [controller, controller],
),
);
expectNothingLearnt(tester, "the phrase's words");
});
}