fix(app): the phrase typed back is kept out of screenshots too

The reading of the full reading's fixes found that the recovery phrase
typed back (recovering, resetting an authenticator, finishing a reset
that keeps the keys) showed all 24 words in a plain field, in Android's
recent apps, screenshots and iOS's app switcher, while people switch
apps to read it; and the three words typed back at sign-up alike.
PhraseField and PhraseCheckFields now show inside a SecretScreen.

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 16:17:26 +02:00
commit fac9a000a1
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 63 additions and 44 deletions

View file

@ -364,7 +364,11 @@ class const PhraseField({
@override
Widget build(BuildContext context) {
final l10n = HalkyonAppLocalizations.of(context);
return TextField(
// Typed back, the whole phrase shows: kept out of screenshots and recent
// apps while it does, as when it was first shown (the reading of the
// fixes).
return SecretScreen(
child: TextField(
controller: controller,
enabled: enabled,
autofocus: autofocus,
@ -381,6 +385,7 @@ class const PhraseField({
hintText: l10n.passwordPhraseFieldHint,
errorText: errorText,
),
),
);
}
}
@ -397,7 +402,9 @@ class const PhraseCheckFields({
@override
Widget build(BuildContext context) {
final l10n = HalkyonAppLocalizations.of(context);
return Column(
// Words of the phrase: kept out of screenshots and recent apps too.
return SecretScreen(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (var i = 0; i < positions.length; i++) ...[
@ -425,6 +432,7 @@ class const PhraseCheckFields({
const SizedBox(height: 8),
],
],
),
);
}
}

View file

@ -46,11 +46,22 @@ void main() {
expect(find.byKey(SecretScreen.coverKey), findsNothing);
});
testWidgets('the phrase, the recovery codes and an authenticator are each '
'shown as a secret', (tester) async {
testWidgets('the phrase, shown or typed back, the recovery codes and an '
'authenticator are each shown as a secret (typed back: the reading of '
'the fixes)', (tester) async {
final code = TextEditingController();
addTearDown(code.dispose);
final typed = TextEditingController();
addTearDown(typed.dispose);
final words = [TextEditingController(), TextEditingController()];
addTearDown(() {
for (final word in words) {
word.dispose();
}
});
for (final view in [
PhraseField(controller: typed),
PhraseCheckFields(positions: const [2, 7], controllers: words),
const RecoveryPhraseView(
phrase:
'abandon abandon abandon abandon abandon abandon abandon abandon '