feat(useragent): initial impl

This commit is contained in:
hdbg
2026-03-04 15:27:27 +01:00
parent ccd657c9ec
commit 62c4bc5ade
78 changed files with 10635 additions and 223 deletions

View File

@@ -0,0 +1,118 @@
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:sizer/sizer.dart';
const transitionDuration = Duration(milliseconds: 800);
class AdaptiveBuilders {
WidgetBuilder? buildSmall;
WidgetBuilder? buildMediumLarge;
WidgetBuilder? buildLarge;
WidgetBuilder? buildExtraLarge;
WidgetBuilder? build;
AdaptiveBuilders({
this.buildSmall,
this.buildMediumLarge,
this.buildLarge,
this.buildExtraLarge,
this.build,
});
}
class Destination {
final String label;
final String? tooltip;
final Icon icon;
final Icon? selectedIcon;
final AdaptiveBuilders main;
final AdaptiveBuilders? secondary;
Destination({
required this.label,
required this.icon,
this.selectedIcon,
required this.main,
this.secondary,
this.tooltip,
});
}
class Switcher extends StatelessWidget {
final Widget? child;
const Switcher({super.key, this.child});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: Duration(
milliseconds: transitionDuration.inMilliseconds ~/ 100,
),
transitionBuilder: (child, animation) {
return FadeTransition(opacity: animation, child: child);
},
child: child,
);
}
}
WidgetBuilder? patchAnimated(WidgetBuilder? input) {
if (input == null) return null;
return (context) => Switcher(child: input(context));
}
class HomeRouter extends HookWidget {
final List<Destination> destinations;
HomeRouter({super.key, required this.destinations})
: assert(destinations.isNotEmpty);
@override
Widget build(BuildContext context) {
final selectedIndex = useState(0);
final destination = useMemoized(() => destinations[selectedIndex.value], [
selectedIndex.value,
]);
final dispatcher = useMemoized(() => destination.main, [
selectedIndex.value,
]);
final secondaryDispatcher = useMemoized(() => destination.secondary, [
selectedIndex.value,
]);
return AdaptiveScaffold(
destinations: destinations
.map(
(destination) => NavigationDestination(
label: destination.label,
icon: destination.icon,
selectedIcon: destination.selectedIcon,
tooltip: destination.tooltip,
),
)
.toList(),
selectedIndex: selectedIndex.value,
onSelectedIndexChange: (index) => selectedIndex.value = index,
useDrawer: true,
smallBody: patchAnimated(dispatcher.buildSmall),
body: patchAnimated(dispatcher.build),
mediumLargeBody: patchAnimated(dispatcher.buildMediumLarge),
largeBody: patchAnimated(dispatcher.buildLarge),
extraLargeBody: patchAnimated(dispatcher.buildExtraLarge),
smallSecondaryBody: patchAnimated(secondaryDispatcher?.buildSmall),
secondaryBody: patchAnimated(secondaryDispatcher?.build),
mediumLargeSecondaryBody: patchAnimated(
secondaryDispatcher?.buildMediumLarge,
),
largeSecondaryBody: patchAnimated(secondaryDispatcher?.buildLarge),
extraLargeSecondaryBody: patchAnimated(
secondaryDispatcher?.buildExtraLarge,
),
);
}
}

View File

@@ -0,0 +1,26 @@
import 'dart:async';
import 'package:arbiter/providers/key.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mtcore/markettakers.dart';
class Bootstrap extends HookConsumerWidget {
final Completer<void> completer;
const Bootstrap({required this.completer});
@override
Widget build(BuildContext context, WidgetRef ref) {
final container = ProviderScope.containerOf(context);
final stages = useMemoized(() {
return [KeyBootstrapper(ref: container)];
}, []);
final bootstrapper = useMemoized(
() => Bootstrapper(stages: stages, onCompleted: completer),
[stages],
);
return bootstrapper;
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/services.dart';
enum KeyAlgorithm {
rsa, ecdsa, ed25519
}
// The API to handle without storing the private key in memory.
//The implementation will use platform-specific secure storage and signing capabilities.
abstract class KeyHandle {
KeyAlgorithm get alg;
Future<List<int>> sign(List<int> data);
Future<List<int>> getPublicKey();
}
abstract class KeyManager {
Future<KeyHandle?> get();
Future<KeyHandle> create();
}

View File

@@ -0,0 +1,94 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:arbiter/features/pk_manager.dart';
final storage = FlutterSecureStorage(
aOptions: AndroidOptions.biometric(
enforceBiometrics: true,
biometricPromptTitle: 'Authentication Required',
),
mOptions: MacOsOptions(
accessibility: KeychainAccessibility.unlocked_this_device,
label: "Arbiter",
description: "Confirm your identity to access vault",
synchronizable: false,
accessControlFlags: [
AccessControlFlag.userPresence,
AccessControlFlag.privateKeyUsage,
],
usesDataProtectionKeychain: true,
),
);
final processor = Ed25519();
class SimpleEd25519 extends KeyHandle {
final SimpleKeyPair _keyPair;
SimpleEd25519({required SimpleKeyPair keyPair}) : _keyPair = keyPair;
@override
KeyAlgorithm get alg => KeyAlgorithm.ed25519;
@override
Future<List<int>> getPublicKey() async {
final publicKey = await _keyPair.extractPublicKey();
return publicKey.bytes;
}
@override
Future<List<int>> sign(List<int> data) async {
final signature = await processor.sign(data, keyPair: _keyPair);
return signature.bytes;
}
}
class SimpleEd25519Manager extends KeyManager {
static const _storageKey = "ed25519_identity";
static const _storagePublicKey = "ed25519_public_key";
@override
Future<KeyHandle> create() async {
final storedKey = await get();
if (storedKey != null) {
return storedKey;
}
final newKey = await processor.newKeyPair();
final rawKey = await newKey.extract();
final keyData = base64Encode(rawKey.bytes);
await storage.write(key: _storageKey, value: keyData);
final publicKeyData = base64Encode(rawKey.publicKey.bytes);
await storage.write(key: _storagePublicKey, value: publicKeyData);
return SimpleEd25519(keyPair: newKey);
}
@override
Future<KeyHandle?> get() async {
final storedKeyPair = await storage.read(key: _storageKey);
if (storedKeyPair == null) {
return null;
}
final publicKeyData = await storage.read(key: _storagePublicKey);
final publicKeyRaw = base64Decode(publicKeyData!);
final publicKey = SimplePublicKey(
publicKeyRaw,
type: processor.keyPairType,
);
final keyBytes = base64Decode(storedKeyPair);
final keypair = SimpleKeyPairData(
keyBytes,
publicKey: publicKey,
type: processor.keyPairType,
);
return SimpleEd25519(keyPair: keypair);
}
}