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:
parent
5067d11c18
commit
c9904de31d
5 changed files with 106 additions and 31 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue