mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-12 17:51:01 +01:00
crdt_sync_flutter 0.7.0: passphrase lock button
Todo: fda71ed1 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
33791da6ff
commit
307248b60b
@ -15,6 +15,7 @@ export 'src/account_store.dart';
|
||||
export 'src/bootstrap.dart';
|
||||
export 'src/device_id.dart';
|
||||
export 'src/e2e_bootstrap.dart';
|
||||
export 'src/e2e_passphrase_button.dart';
|
||||
export 'src/google_any_sign_in.dart';
|
||||
export 'src/google_desktop_sign_in.dart';
|
||||
export 'src/google_pkce.dart';
|
||||
|
||||
77
crdt_sync_flutter/lib/src/e2e_passphrase_button.dart
Normal file
77
crdt_sync_flutter/lib/src/e2e_passphrase_button.dart
Normal file
@ -0,0 +1,77 @@
|
||||
/// A text-free control to enter the shared crdt-sync passphrase: a lock icon
|
||||
/// opens a hidden field; the check icon (or Enter) saves it to the keystore.
|
||||
library;
|
||||
|
||||
import 'package:crdt_sync_flutter/src/e2e_bootstrap.dart';
|
||||
import 'package:crdt_sync_flutter/src/keystore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
/// Lock icon button; [locked] picks the closed or open lock glyph.
|
||||
class E2ePassphraseButton extends StatelessWidget {
|
||||
/// Creates the button. [onSaved] runs after the passphrase is stored.
|
||||
const E2ePassphraseButton({
|
||||
required this.locked,
|
||||
this.onSaved,
|
||||
this.storage = kSecureStorage,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Whether sync is currently locked (no or wrong passphrase).
|
||||
final bool locked;
|
||||
|
||||
/// Called after a passphrase is saved, e.g. to retry the sync.
|
||||
final VoidCallback? onSaved;
|
||||
|
||||
/// The keystore; only tests pass another.
|
||||
final FlutterSecureStorage storage;
|
||||
|
||||
Future<void> _ask(BuildContext context) async {
|
||||
final entered = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => const _PassphraseDialog(),
|
||||
);
|
||||
if (entered == null || entered.isEmpty) return;
|
||||
await saveE2ePassphrase(entered, storage: storage);
|
||||
onSaved?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => IconButton(
|
||||
icon: Icon(locked ? Icons.lock : Icons.lock_open),
|
||||
onPressed: () => _ask(context),
|
||||
);
|
||||
}
|
||||
|
||||
class _PassphraseDialog extends StatefulWidget {
|
||||
const _PassphraseDialog();
|
||||
|
||||
@override
|
||||
State<_PassphraseDialog> createState() => _PassphraseDialogState();
|
||||
}
|
||||
|
||||
class _PassphraseDialogState extends State<_PassphraseDialog> {
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AlertDialog(
|
||||
content: TextField(
|
||||
controller: _controller,
|
||||
obscureText: true,
|
||||
autofocus: true,
|
||||
onSubmitted: (value) => Navigator.of(context).pop(value),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () => Navigator.of(context).pop(_controller.text),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -4,7 +4,7 @@ description: >-
|
||||
persisted per-install device id, and a one-call bootstrap. Removes the
|
||||
~150 lines of firebase_backend.dart every consuming app was copying.
|
||||
publish_to: 'none'
|
||||
version: 0.6.1
|
||||
version: 0.7.0
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
@ -88,7 +88,11 @@ void main() {
|
||||
final remote = _Remote();
|
||||
await saveE2ePassphrase('pw', storage: storage);
|
||||
final first =
|
||||
await openEncryptedStoreFromKeystore(store: remote, oldPrefix: _old, params: _cheap)
|
||||
await openEncryptedStoreFromKeystore(
|
||||
store: remote,
|
||||
oldPrefix: _old,
|
||||
params: _cheap,
|
||||
)
|
||||
as E2eReady;
|
||||
final layout = E2eLayout.of(_old);
|
||||
await first.store.putFileText(
|
||||
@ -97,7 +101,11 @@ void main() {
|
||||
message: 'm',
|
||||
);
|
||||
final second =
|
||||
await openEncryptedStoreFromKeystore(store: remote, oldPrefix: _old, params: _cheap)
|
||||
await openEncryptedStoreFromKeystore(
|
||||
store: remote,
|
||||
oldPrefix: _old,
|
||||
params: _cheap,
|
||||
)
|
||||
as E2eReady;
|
||||
final text = await second.store.getFileText(
|
||||
'${layout.prefix}/phone/log.json',
|
||||
@ -108,7 +116,11 @@ void main() {
|
||||
test('wrong passphrase is locked and merges nothing', () async {
|
||||
final remote = _Remote();
|
||||
await saveE2ePassphrase('pw', storage: storage);
|
||||
await openEncryptedStoreFromKeystore(store: remote, oldPrefix: _old, params: _cheap);
|
||||
await openEncryptedStoreFromKeystore(
|
||||
store: remote,
|
||||
oldPrefix: _old,
|
||||
params: _cheap,
|
||||
);
|
||||
await saveE2ePassphrase('nope', storage: storage);
|
||||
final state = await openEncryptedStoreFromKeystore(
|
||||
store: remote,
|
||||
|
||||
65
crdt_sync_flutter/test/e2e_passphrase_button_test.dart
Normal file
65
crdt_sync_flutter/test/e2e_passphrase_button_test.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import 'package:crdt_sync_flutter/crdt_sync_flutter.dart';
|
||||
import 'package:crdt_sync_flutter/testing/fake_secure_storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUp(installFakeSecureStorage);
|
||||
|
||||
Future<void> pump(WidgetTester t, {required bool locked, VoidCallback? cb}) =>
|
||||
t.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: E2ePassphraseButton(locked: locked, onSaved: cb),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('icon follows the locked flag', (t) async {
|
||||
await pump(t, locked: true);
|
||||
expect(find.byIcon(Icons.lock), findsOneWidget);
|
||||
await pump(t, locked: false);
|
||||
expect(find.byIcon(Icons.lock_open), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('entering a passphrase stores it and notifies', (t) async {
|
||||
var saved = 0;
|
||||
await pump(t, locked: true, cb: () => saved++);
|
||||
await t.tap(find.byType(IconButton));
|
||||
await t.pumpAndSettle();
|
||||
await t.enterText(find.byType(TextField), 'pw');
|
||||
await t.tap(find.byIcon(Icons.check));
|
||||
await t.pumpAndSettle();
|
||||
expect(
|
||||
await const FlutterSecureStorage().read(key: kE2ePassphraseKey),
|
||||
'pw',
|
||||
);
|
||||
expect(saved, 1);
|
||||
});
|
||||
|
||||
testWidgets('submitting with Enter saves; empty input saves nothing', (
|
||||
t,
|
||||
) async {
|
||||
await pump(t, locked: true);
|
||||
await t.tap(find.byType(IconButton));
|
||||
await t.pumpAndSettle();
|
||||
await t.enterText(find.byType(TextField), 'x1');
|
||||
await t.testTextInput.receiveAction(TextInputAction.done);
|
||||
await t.pumpAndSettle();
|
||||
expect(
|
||||
await const FlutterSecureStorage().read(key: kE2ePassphraseKey),
|
||||
'x1',
|
||||
);
|
||||
await clearE2ePassphrase();
|
||||
await t.tap(find.byType(IconButton));
|
||||
await t.pumpAndSettle();
|
||||
await t.tap(find.byIcon(Icons.check));
|
||||
await t.pumpAndSettle();
|
||||
expect(
|
||||
await const FlutterSecureStorage().read(key: kE2ePassphraseKey),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user