fix(app): the phrase is never copied, codes leave the clipboard a minute later

The recovery phrase opens the account's keys; clipboard histories and
cloud clipboard sync would keep it beyond the app's reach. It is saved as
a file or written down. Recovery codes and an authenticator's key may be
copied, and the clipboard is emptied 60 s later if it still holds them;
they are no longer selectable, as the system's own copy wouldn't empty it.

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:31:19 +02:00
commit a0b7e2bf18
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,103 @@
import 'package:flutter/services.dart';
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() {
/// What the system's clipboard holds.
String? clipboard;
/// Stands in for the system's clipboard for [tester].
void mockClipboard(WidgetTester tester) {
clipboard = null;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async => switch (call.method) {
'Clipboard.setData' => () {
clipboard = (call.arguments as Map)['text'] as String?;
return null;
}(),
'Clipboard.getData' => {'text': clipboard},
_ => null,
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
),
);
}
Future<void> pumpView(WidgetTester tester, Widget view) async {
tester.view.physicalSize = const Size(412, 1400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
AppScope(
dependencies: TestAppDependencies(gateway: FakeAuthGateway()),
child: MaterialApp(
localizationsDelegates:
HalkyonAppLocalizations.localizationsDelegates,
supportedLocales: HalkyonAppLocalizations.supportedLocales,
home: Scaffold(body: SingleChildScrollView(child: view)),
),
),
);
}
group('copySecret', () {
testWidgets('clears the clipboard a minute later', (tester) async {
mockClipboard(tester);
await copySecret('ABCD-EFGH');
expect(clipboard, 'ABCD-EFGH');
await tester.pump(const Duration(seconds: 59));
expect(clipboard, 'ABCD-EFGH');
await tester.pump(const Duration(seconds: 1));
expect(clipboard, '');
});
testWidgets('leaves what was copied since', (tester) async {
mockClipboard(tester);
await copySecret('ABCD-EFGH');
clipboard = 'something else';
await tester.pump(secretClipboardLifetime);
expect(clipboard, 'something else');
});
});
testWidgets('the recovery phrase is saved, never copied', (tester) async {
mockClipboard(tester);
await pumpView(
tester,
const RecoveryPhraseView(
phrase:
'abandon abandon abandon abandon abandon abandon abandon abandon '
'abandon abandon abandon abandon abandon abandon abandon abandon '
'abandon abandon abandon abandon abandon abandon abandon art',
),
);
expect(find.text('Copy'), findsNothing);
expect(find.text('Save as a file'), findsOneWidget);
expect(find.byType(SelectableText), findsNothing);
});
testWidgets('recovery codes are copied, and cleared a minute later', (
tester,
) async {
mockClipboard(tester);
await pumpView(
tester,
const RecoveryCodesView(codes: ['AAAA-BBBB', 'CCCC-DDDD']),
);
expect(find.byType(SelectableText), findsNothing);
await tester.tap(find.text('Copy'));
await tester.pump();
expect(clipboard, 'AAAA-BBBB\nCCCC-DDDD');
await tester.pump(secretClipboardLifetime);
expect(clipboard, '');
// The snack bar's own time.
await tester.pumpAndSettle();
});
}