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}) {
|
Widget _phraseField(bool enabled, {String? errorText}) =>
|
||||||
final l10n = HalkyonAppLocalizations.of(context);
|
PhraseField(controller: _phrase, enabled: enabled, errorText: errorText);
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
||||||
|
|
@ -266,21 +266,12 @@ class _PasswordSignInState() extends State<_PasswordSignIn> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SheetIntro(l10n.passwordPhraseFieldIntro),
|
SheetIntro(l10n.passwordPhraseFieldIntro),
|
||||||
TextField(
|
PhraseField(
|
||||||
controller: _phrase,
|
controller: _phrase,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
maxLines: 5,
|
errorText: problem,
|
||||||
maxLength: TextLimits.passage,
|
|
||||||
buildCounter: nearLimitCounter,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.passwordPhraseField,
|
|
||||||
hintText: l10n.passwordPhraseFieldHint,
|
|
||||||
errorText: problem,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ class _PasswordFieldState() extends State<PasswordField> {
|
||||||
obscureText: !_shown,
|
obscureText: !_shown,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
|
// Nothing typed here is the keyboard's to learn.
|
||||||
|
enableIMEPersonalizedLearning: false,
|
||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
autofillHints: [
|
autofillHints: [
|
||||||
if (widget.isNew)
|
if (widget.isNew)
|
||||||
|
|
@ -155,6 +157,8 @@ class const CodeField({
|
||||||
autofillHints: const [AutofillHints.oneTimeCode],
|
autofillHints: const [AutofillHints.oneTimeCode],
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
|
// Nothing typed here is the keyboard's to learn.
|
||||||
|
enableIMEPersonalizedLearning: false,
|
||||||
keyboardType: digits ? TextInputType.number : TextInputType.text,
|
keyboardType: digits ? TextInputType.number : TextInputType.text,
|
||||||
textCapitalization: TextCapitalization.characters,
|
textCapitalization: TextCapitalization.characters,
|
||||||
textInputAction: textInputAction,
|
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).
|
/// Fields for the words of a recovery phrase at [positions] (from 0).
|
||||||
class const PhraseCheckFields({
|
class const PhraseCheckFields({
|
||||||
required final List<int> positions,
|
required final List<int> positions,
|
||||||
|
|
@ -379,6 +418,8 @@ class const PhraseCheckFields({
|
||||||
autofocus: i == 0,
|
autofocus: i == 0,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
|
// Nothing typed here is the keyboard's to learn.
|
||||||
|
enableIMEPersonalizedLearning: false,
|
||||||
textInputAction: i == positions.length - 1
|
textInputAction: i == positions.length - 1
|
||||||
? TextInputAction.done
|
? TextInputAction.done
|
||||||
: TextInputAction.next,
|
: TextInputAction.next,
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,7 @@ void main() {
|
||||||
AppScope(
|
AppScope(
|
||||||
dependencies: TestAppDependencies(gateway: FakeAuthGateway()),
|
dependencies: TestAppDependencies(gateway: FakeAuthGateway()),
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
localizationsDelegates:
|
localizationsDelegates: testLocalizationsDelegates,
|
||||||
HalkyonAppLocalizations.localizationsDelegates,
|
|
||||||
supportedLocales: HalkyonAppLocalizations.supportedLocales,
|
supportedLocales: HalkyonAppLocalizations.supportedLocales,
|
||||||
home: Scaffold(body: SingleChildScrollView(child: view)),
|
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