fix(app): the phrase typed back is pasted into its fields, never copied out

The fields the recovery phrase is typed back into offered the system's
Copy, Cut and Share, and Ctrl+C and Ctrl+X copied what was selected (the
short reading's L3), so the phrase could still reach clipboard histories,
cloud clipboards or another app. Their menu now offers Paste and Select
all alone, and the field's own copy and cut, overridden from above, do
nothing. The comment on the recovery codes no longer speaks of Copy.

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 18:01:03 +02:00
commit 87529cd5c6
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -368,22 +368,25 @@ class const PhraseField({
// 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,
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,
child: _PasteOnly(
child: TextField(
controller: controller,
enabled: enabled,
autofocus: autofocus,
autocorrect: false,
enableSuggestions: false,
// Nothing typed here is the keyboard's to learn.
enableIMEPersonalizedLearning: false,
contextMenuBuilder: _PasteOnly.menu,
minLines: minLines,
maxLines: 5,
maxLength: TextLimits.passage,
buildCounter: nearLimitCounter,
decoration: InputDecoration(
labelText: l10n.passwordPhraseField,
hintText: l10n.passwordPhraseFieldHint,
errorText: errorText,
),
),
),
);
@ -408,25 +411,28 @@ class const PhraseCheckFields({
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (var i = 0; i < positions.length; i++) ...[
TextField(
controller: controllers[i],
enabled: enabled,
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,
onSubmitted: i == positions.length - 1
? (_) => onDone?.call()
: null,
maxLength: TextLimits.name,
buildCounter: nearLimitCounter,
decoration: InputDecoration(
labelText: l10n.passwordPhraseWord(positions[i] + 1),
errorText: i == positions.length - 1 ? errorText : null,
_PasteOnly(
child: TextField(
controller: controllers[i],
enabled: enabled,
autofocus: i == 0,
autocorrect: false,
enableSuggestions: false,
// Nothing typed here is the keyboard's to learn.
enableIMEPersonalizedLearning: false,
contextMenuBuilder: _PasteOnly.menu,
textInputAction: i == positions.length - 1
? TextInputAction.done
: TextInputAction.next,
onSubmitted: i == positions.length - 1
? (_) => onDone?.call()
: null,
maxLength: TextLimits.name,
buildCounter: nearLimitCounter,
decoration: InputDecoration(
labelText: l10n.passwordPhraseWord(positions[i] + 1),
errorText: i == positions.length - 1 ? errorText : null,
),
),
),
const SizedBox(height: 8),
@ -437,6 +443,35 @@ class const PhraseCheckFields({
}
}
/// [child], a field the recovery phrase is typed into, from which nothing
/// is copied, cut or shared (the short reading's L3): its menu offers
/// Paste and Select all alone ([menu], which the field takes as its
/// `contextMenuBuilder`), and the keys that would copy or cut do nothing.
/// The phrase is typed back to show it was kept; a copy of it would reach
/// the clipboard's histories as one shown would (`KeepTextButtons`).
class const _PasteOnly({required final Widget child}) extends StatelessWidget {
/// A field's menu with Paste and Select all alone.
static Widget menu(BuildContext context, EditableTextState state) =>
AdaptiveTextSelectionToolbar.buttonItems(
anchors: state.contextMenuAnchors,
buttonItems: [
for (final item in state.contextMenuButtonItems)
if (item.type == ContextMenuButtonType.paste ||
item.type == ContextMenuButtonType.selectAll)
item,
],
);
@override
Widget build(BuildContext context) => Actions(
// The field's own copy and cut can be overridden from above.
actions: <Type, Action<Intent>>{
CopySelectionTextIntent: DoNothingAction(),
},
child: child,
);
}
/// Ten recovery codes, to keep.
class const RecoveryCodesView({required final List<String> codes, super.key})
extends StatelessWidget {
@ -459,8 +494,8 @@ class const RecoveryCodesView({required final List<String> codes, super.key})
spacing: 24,
runSpacing: 8,
children: [
// Not selectable: Copy empties the clipboard again, the
// system's own copy wouldn't.
// Not selectable: nothing secret is copied, saved or
// written down instead ([KeepTextButtons]).
for (final code in codes)
Text(
code,