fix(useragent): unsafe, but working implementation of ml-dsa

This commit is contained in:
hdbg
2026-04-07 15:41:50 +02:00
parent 6b8da567dd
commit a4070e7df7
104 changed files with 11133 additions and 461 deletions

View File

@@ -18,7 +18,6 @@ sealed class CalloutEvent with _$CalloutEvent {
required CalloutData data,
}) = CalloutEventAdded;
const factory CalloutEvent.cancelled({
required String id,
}) = CalloutEventCancelled;
const factory CalloutEvent.cancelled({required String id}) =
CalloutEventCancelled;
}

View File

@@ -14,11 +14,8 @@ Future<void> showCallout(BuildContext context, WidgetRef ref, String id) async {
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 320),
pageBuilder: (_, animation, _) => _CalloutOverlay(
id: id,
data: data,
animation: animation,
),
pageBuilder: (_, animation, _) =>
_CalloutOverlay(id: id, data: data, animation: animation),
);
}
@@ -35,22 +32,25 @@ class _CalloutOverlay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(
calloutManagerProvider.select((map) => map.containsKey(id)),
(wasPresent, isPresent) {
if (wasPresent == true && !isPresent && context.mounted) {
Navigator.of(context).pop();
}
},
);
ref.listen(calloutManagerProvider.select((map) => map.containsKey(id)), (
wasPresent,
isPresent,
) {
if (wasPresent == true && !isPresent && context.mounted) {
Navigator.of(context).pop();
}
});
final content = switch (data) {
ConnectApprovalData(:final pubkey, :final clientInfo) => SdkConnectCallout(
pubkey: pubkey,
clientInfo: clientInfo,
onAccept: () => ref.read(calloutManagerProvider.notifier).sendDecision(id, true),
onDecline: () => ref.read(calloutManagerProvider.notifier).sendDecision(id, false),
),
ConnectApprovalData(:final pubkey, :final clientInfo) =>
SdkConnectCallout(
pubkey: pubkey,
clientInfo: clientInfo,
onAccept: () =>
ref.read(calloutManagerProvider.notifier).sendDecision(id, true),
onDecline: () =>
ref.read(calloutManagerProvider.notifier).sendDecision(id, false),
),
};
final barrierAnim = CurvedAnimation(

View File

@@ -14,7 +14,8 @@ Future<void> showCalloutList(BuildContext context, WidgetRef ref) async {
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 280),
pageBuilder: (_, animation, __) => _CalloutListOverlay(animation: animation),
pageBuilder: (_, animation, __) =>
_CalloutListOverlay(animation: animation),
);
if (selectedId != null && context.mounted) {
@@ -51,7 +52,9 @@ class _CalloutListOverlay extends ConsumerWidget {
child: AnimatedBuilder(
animation: barrierAnim,
builder: (_, __) => ColoredBox(
color: Colors.black.withValues(alpha: 0.35 * barrierAnim.value),
color: Colors.black.withValues(
alpha: 0.35 * barrierAnim.value,
),
),
),
),

View File

@@ -50,7 +50,9 @@ class ArbiterUrl {
try {
return base64Url.decode(base64Url.normalize(cert));
} on FormatException catch (error) {
throw FormatException("Invalid base64 in 'cert' query parameter: ${error.message}");
throw FormatException(
"Invalid base64 in 'cert' query parameter: ${error.message}",
);
}
}
}

View File

@@ -101,7 +101,9 @@ Future<Connection> connectAndAuthorize(
final solutionResponse = await connection.ask(
UserAgentRequest(
auth: ua_auth.Request(
challengeSolution: ua_auth.AuthChallengeSolution(signature: signature),
challengeSolution: ua_auth.AuthChallengeSolution(
signature: signature,
),
),
),
);

View File

@@ -85,7 +85,9 @@ class Connection {
if (response.hasId()) {
final completer = _pendingRequests.remove(response.id);
if (completer == null) {
talker.warning('Received response for unknown request id ${response.id}');
talker.warning(
'Received response for unknown request id ${response.id}',
);
return;
}
completer.complete(response);

View File

@@ -9,9 +9,7 @@ Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
UserAgentRequest(evm: ua_evm.Request(walletList: Empty())),
);
if (!response.hasEvm()) {
throw Exception(
'Expected EVM response, got ${response.whichPayload()}',
);
throw Exception('Expected EVM response, got ${response.whichPayload()}');
}
final evmResponse = response.evm;
@@ -37,9 +35,7 @@ Future<void> createEvmWallet(Connection connection) async {
UserAgentRequest(evm: ua_evm.Request(walletCreate: Empty())),
);
if (!response.hasEvm()) {
throw Exception(
'Expected EVM response, got ${response.whichPayload()}',
);
throw Exception('Expected EVM response, got ${response.whichPayload()}');
}
final evmResponse = response.evm;

View File

@@ -10,9 +10,7 @@ Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
UserAgentRequest(evm: ua_evm.Request(grantList: request)),
);
if (!response.hasEvm()) {
throw Exception(
'Expected EVM response, got ${response.whichPayload()}',
);
throw Exception('Expected EVM response, got ${response.whichPayload()}');
}
final evmResponse = response.evm;
@@ -50,9 +48,7 @@ Future<int> createEvmGrant(
final resp = await connection.ask(request);
if (!resp.hasEvm()) {
throw Exception(
'Expected EVM response, got ${resp.whichPayload()}',
);
throw Exception('Expected EVM response, got ${resp.whichPayload()}');
}
final evmResponse = resp.evm;
@@ -70,15 +66,11 @@ Future<int> createEvmGrant(
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
final response = await connection.ask(
UserAgentRequest(
evm: ua_evm.Request(
grantDelete: EvmGrantDeleteRequest(grantId: grantId),
),
evm: ua_evm.Request(grantDelete: EvmGrantDeleteRequest(grantId: grantId)),
),
);
if (!response.hasEvm()) {
throw Exception(
'Expected EVM response, got ${response.whichPayload()}',
);
throw Exception('Expected EVM response, got ${response.whichPayload()}');
}
final evmResponse = response.evm;

View File

@@ -8,9 +8,7 @@ Future<Set<int>> readClientWalletAccess(
required int clientId,
}) async {
final response = await connection.ask(
UserAgentRequest(
sdkClient: ua_sdk.Request(listWalletAccess: Empty()),
),
UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())),
);
if (!response.hasSdkClient()) {
throw Exception(
@@ -33,9 +31,7 @@ Future<List<ua_sdk.WalletAccessEntry>> listAllWalletAccesses(
Connection connection,
) async {
final response = await connection.ask(
UserAgentRequest(
sdkClient: ua_sdk.Request(listWalletAccess: Empty()),
),
UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())),
);
if (!response.hasSdkClient()) {
throw Exception(
@@ -81,9 +77,7 @@ Future<void> writeClientWalletAccess(
UserAgentRequest(
sdkClient: ua_sdk.Request(
revokeWalletAccess: ua_sdk.RevokeWalletAccess(
accesses: [
for (final walletId in toRevoke) walletId,
],
accesses: [for (final walletId in toRevoke) walletId],
),
),
),

View File

@@ -17,9 +17,9 @@ class StoredServerInfo {
final int port;
final String caCertFingerprint;
factory StoredServerInfo.fromJson(Map<String, dynamic> json) => _$StoredServerInfoFromJson(json);
factory StoredServerInfo.fromJson(Map<String, dynamic> json) =>
_$StoredServerInfoFromJson(json);
Map<String, dynamic> toJson() => _$StoredServerInfoToJson(this);
}
abstract class ServerInfoStorage {

View File

@@ -1,5 +1,6 @@
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/proto/user_agent/vault/bootstrap.pb.dart' as ua_bootstrap;
import 'package:arbiter/proto/user_agent/vault/bootstrap.pb.dart'
as ua_bootstrap;
import 'package:arbiter/proto/user_agent/vault/unseal.pb.dart' as ua_unseal;
import 'package:arbiter/proto/user_agent/vault/vault.pb.dart' as ua_vault;
import 'package:arbiter/proto/user_agent.pb.dart';
@@ -27,9 +28,7 @@ Future<ua_bootstrap.BootstrapResult> bootstrapVault(
),
);
if (!response.hasVault()) {
throw Exception(
'Expected vault response, got ${response.whichPayload()}',
);
throw Exception('Expected vault response, got ${response.whichPayload()}');
}
final vaultResponse = response.vault;

View File

@@ -0,0 +1,71 @@
import 'dart:convert';
import 'package:arbiter/src/rust/api.dart';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:arbiter/features/identity/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],
usesDataProtectionKeychain: true,
),
);
class HazmatMldsa extends KeyHandle {
final MldsaKey _key;
HazmatMldsa({required MldsaKey key}) : _key = key;
@override
Future<List<int>> getPublicKey() async {
final publicKey = await _key.getPublicKey();
return publicKey;
}
@override
Future<List<int>> sign(List<int> data) async {
final signature = await _key.sign(message: data);
return signature;
}
}
class HazmatMLDSAManager extends KeyManager {
static const _storageKey = "ed25519_identity";
@override
Future<KeyHandle> create() async {
final storedKey = await get();
if (storedKey != null) {
return storedKey;
}
final newKeypair = await MldsaKey.generate();
final keyBytes = await newKeypair.toBytes();
await storage.write(key: _storageKey, value: base64Encode(keyBytes));
return HazmatMldsa(key: newKeypair);
}
@override
Future<KeyHandle?> get() async {
final storedKeyPair = await storage.read(key: _storageKey);
if (storedKeyPair == null) {
return null;
}
final keyBytes = base64Decode(storedKeyPair);
final key = await MldsaKey.fromBytes(bytes: keyBytes);
return HazmatMldsa(key: key);
}
}

View File

@@ -1,11 +1,6 @@
enum KeyAlgorithm {
rsa, ecdsa, ed25519
}
// The API to handle without storing the private key in memory.
// 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();
}

View File

@@ -1,93 +0,0 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:arbiter/features/identity/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,
],
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);
}
}

View File

@@ -1,10 +1,12 @@
import 'package:arbiter/router.dart';
import 'package:arbiter/src/rust/frb_generated.dart';
import 'package:flutter/material.dart' hide Router;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
void main() {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
runApp(const ProviderScope(child: App()));
}
@@ -33,3 +35,5 @@ class _AppState extends State<App> {
);
}
}

View File

@@ -1,5 +1,3 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bootstrap_token.g.dart';

View File

@@ -2,7 +2,6 @@ import 'package:arbiter/features/connection/evm.dart' as evm;
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'evm.g.dart';
@@ -35,7 +34,7 @@ final createEvmWallet = Mutation();
Future<void> executeCreateEvmWallet(MutationTarget target) async {
return await createEvmWallet.run(target, (tsx) async {
final connection = await tsx.get(connectionManagerProvider.future);
final connection = await tsx.get(connectionManagerProvider.future);
if (connection == null) {
throw Exception('Not connected to the server.');
}
@@ -44,4 +43,4 @@ Future<void> executeCreateEvmWallet(MutationTarget target) async {
await tsx.get(evmProvider.notifier).refreshWallets();
});
}
}

View File

@@ -1,13 +1,13 @@
import 'package:mtcore/markettakers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:arbiter/features/identity/pk_manager.dart';
import 'package:arbiter/features/identity/simple_ed25519.dart';
import 'package:arbiter/features/identity/hazmat_mldsa.dart';
part 'key.g.dart';
@riverpod
KeyManager keyManager(Ref ref) {
return SimpleEd25519Manager();
return HazmatMLDSAManager();
}
@Riverpod(keepAlive: true)

View File

@@ -18,9 +18,7 @@ Future<List<ua_sdk.Entry>?> sdkClients(Ref ref) async {
);
if (!resp.hasSdkClient()) {
throw Exception(
'Expected SDK client response, got ${resp.whichPayload()}',
);
throw Exception('Expected SDK client response, got ${resp.whichPayload()}');
}
final sdkClientResponse = resp.sdkClient;
if (!sdkClientResponse.hasList()) {

View File

@@ -46,6 +46,8 @@ class ServerInfo extends _$ServerInfo {
Future<String> _fingerprint(List<int> caCert) async {
final digest = await Sha256().hash(caCert);
return digest.bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
return digest.bytes
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join();
}
}

View File

@@ -28,8 +28,7 @@ class SdkConnectCallout extends StatelessWidget {
final hasDescription =
clientInfo.hasDescription() && clientInfo.description.isNotEmpty;
final hasVersion =
clientInfo.hasVersion() && clientInfo.version.isNotEmpty;
final hasVersion = clientInfo.hasVersion() && clientInfo.version.isNotEmpty;
final showInfoCard = hasDescription || hasVersion;
return CreamFrame(
@@ -74,10 +73,7 @@ class SdkConnectCallout extends StatelessWidget {
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Palette.line),
),
padding: EdgeInsets.symmetric(
horizontal: 1.6.w,
vertical: 1.2.h,
),
padding: EdgeInsets.symmetric(horizontal: 1.6.w, vertical: 1.2.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 0.6.h,

View File

@@ -78,7 +78,7 @@ class DashboardRouter extends StatelessWidget {
}
class _CalloutBell extends ConsumerWidget {
const _CalloutBell({super.key});
const _CalloutBell();
@override
Widget build(BuildContext context, WidgetRef ref) {

View File

@@ -1,4 +1,3 @@
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@@ -13,5 +12,4 @@ class ClientDetails extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
throw UnimplementedError();
}
}

View File

@@ -12,30 +12,24 @@ class ClientSummaryCard extends StatelessWidget {
return CreamFrame(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
client.info.name,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(client.info.description),
const SizedBox(height: 16),
Wrap(
runSpacing: 8,
spacing: 16,
children: [
_Fact(label: 'Client ID', value: '${client.id}'),
_Fact(label: 'Version', value: client.info.version),
_Fact(
label: 'Registered',
value: _formatDate(client.createdAt),
),
_Fact(label: 'Pubkey', value: _shortPubkey(client.pubkey)),
],
),
],
),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(client.info.name, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text(client.info.description),
const SizedBox(height: 16),
Wrap(
runSpacing: 8,
spacing: 16,
children: [
_Fact(label: 'Client ID', value: '${client.id}'),
_Fact(label: 'Version', value: client.info.version),
_Fact(label: 'Registered', value: _formatDate(client.createdAt)),
_Fact(label: 'Pubkey', value: _shortPubkey(client.pubkey)),
],
),
],
),
);
}
}

View File

@@ -28,27 +28,27 @@ class WalletAccessSaveBar extends StatelessWidget {
return CreamFrame(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (errorText != null) ...[
Text(errorText, style: TextStyle(color: Palette.coral)),
const SizedBox(height: 12),
],
Row(
children: [
TextButton(
onPressed: state.hasChanges && !isPending ? onDiscard : null,
child: const Text('Reset'),
),
const Spacer(),
FilledButton(
onPressed: state.hasChanges && !isPending ? onSave : null,
child: Text(isPending ? 'Saving...' : 'Save changes'),
),
],
),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (errorText != null) ...[
Text(errorText, style: TextStyle(color: Palette.coral)),
const SizedBox(height: 12),
],
),
Row(
children: [
TextButton(
onPressed: state.hasChanges && !isPending ? onDiscard : null,
child: const Text('Reset'),
),
const Spacer(),
FilledButton(
onPressed: state.hasChanges && !isPending ? onSave : null,
child: Text(isPending ? 'Saving...' : 'Save changes'),
),
],
),
],
),
);
}
}

View File

@@ -30,26 +30,23 @@ class WalletAccessSection extends ConsumerWidget {
return CreamFrame(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Wallet access',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text('Choose which managed wallets this client can see.'),
const SizedBox(height: 16),
_WalletAccessBody(
clientId: clientId,
state: state,
accessSelectionAsync: accessSelectionAsync,
isSavePending: isSavePending,
optionsAsync: optionsAsync,
onSearchChanged: onSearchChanged,
onToggleWallet: onToggleWallet,
),
],
),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Wallet access', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('Choose which managed wallets this client can see.'),
const SizedBox(height: 16),
_WalletAccessBody(
clientId: clientId,
state: state,
accessSelectionAsync: accessSelectionAsync,
isSavePending: isSavePending,
optionsAsync: optionsAsync,
onSearchChanged: onSearchChanged,
onToggleWallet: onToggleWallet,
),
],
),
);
}
}

View File

@@ -378,48 +378,48 @@ class _ClientTable extends StatelessWidget {
builder: (context, constraints) {
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Registered clients',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Registered clients',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
SizedBox(height: 0.6.h),
Text(
'Every entry here has authenticated with Arbiter at least once.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
),
SizedBox(height: 0.6.h),
Text(
'Every entry here has authenticated with Arbiter at least once.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _ClientTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < clients.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == clients.length - 1 ? 0 : 1.h,
),
child: _ClientTableRow(client: clients[i]),
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _ClientTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < clients.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == clients.length - 1 ? 0 : 1.h,
),
],
),
child: _ClientTableRow(client: clients[i]),
),
],
),
),
],
);
},
),
),
],
);
},
),
);
}
}

View File

@@ -31,7 +31,9 @@ class ClientPickerField extends ConsumerWidget {
? null
: (value) {
ref.read(grantCreationProvider.notifier).setClientId(value);
FormBuilder.of(context)?.fields['walletAccessId']?.didChange(null);
FormBuilder.of(
context,
)?.fields['walletAccessId']?.didChange(null);
},
);
}

View File

@@ -16,46 +16,48 @@ class FormBuilderDateTimeField extends FormBuilderField<DateTime?> {
super.onChanged,
super.validator,
}) : super(
builder: (FormFieldState<DateTime?> field) {
final value = field.value;
return OutlinedButton(
onPressed: () async {
final ctx = field.context;
final now = DateTime.now();
final date = await showDatePicker(
context: ctx,
firstDate: DateTime(now.year - 5),
lastDate: DateTime(now.year + 10),
initialDate: value ?? now,
);
if (date == null) return;
if (!ctx.mounted) return;
final time = await showTimePicker(
context: ctx,
initialTime: TimeOfDay.fromDateTime(value ?? now),
);
if (time == null) return;
field.didChange(DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
));
},
onLongPress: value == null ? null : () => field.didChange(null),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 1.8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
SizedBox(height: 0.6.h),
Text(value?.toLocal().toString() ?? 'Not set'),
],
),
),
);
},
);
builder: (FormFieldState<DateTime?> field) {
final value = field.value;
return OutlinedButton(
onPressed: () async {
final ctx = field.context;
final now = DateTime.now();
final date = await showDatePicker(
context: ctx,
firstDate: DateTime(now.year - 5),
lastDate: DateTime(now.year + 10),
initialDate: value ?? now,
);
if (date == null) return;
if (!ctx.mounted) return;
final time = await showTimePicker(
context: ctx,
initialTime: TimeOfDay.fromDateTime(value ?? now),
);
if (time == null) return;
field.didChange(
DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
),
);
},
onLongPress: value == null ? null : () => field.didChange(null),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 1.8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
SizedBox(height: 0.6.h),
Text(value?.toLocal().toString() ?? 'Not set'),
],
),
),
);
},
);
}

View File

@@ -36,8 +36,8 @@ class WalletAccessPickerField extends ConsumerWidget {
helperText: state.selectedClientId == null
? 'Select a client first'
: accesses.isEmpty
? 'No wallet accesses for this client'
: null,
? 'No wallet accesses for this client'
: null,
border: const OutlineInputBorder(),
),
items: [

View File

@@ -93,9 +93,9 @@ class _EtherTransferForm extends ConsumerWidget {
SizedBox(height: 1.6.h),
Text(
'Ether volume limit',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800),
),
SizedBox(height: 0.8.h),
Row(
@@ -157,9 +157,9 @@ class _EtherTargetsField extends StatelessWidget {
Expanded(
child: Text(
'Ether targets',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800),
),
),
TextButton.icon(

View File

@@ -13,7 +13,11 @@ import 'package:sizer/sizer.dart';
part 'token_transfer_grant.g.dart';
class VolumeLimitEntry {
VolumeLimitEntry({required this.id, this.amount = '', this.windowSeconds = ''});
VolumeLimitEntry({
required this.id,
this.amount = '',
this.windowSeconds = '',
});
final int id;
final String amount;
@@ -27,7 +31,6 @@ class VolumeLimitEntry {
);
}
@riverpod
class TokenGrantLimits extends _$TokenGrantLimits {
int _nextId = 0;
@@ -47,7 +50,6 @@ class TokenGrantLimits extends _$TokenGrantLimits {
void remove(int index) => state = [...state]..removeAt(index);
}
class TokenTransferGrantHandler implements GrantFormHandler {
const TokenTransferGrantHandler();
@@ -65,11 +67,16 @@ class TokenTransferGrantHandler implements GrantFormHandler {
return SpecificGrant(
tokenTransfer: TokenTransferSettings(
tokenContract:
parseHexAddress(formValues['tokenContract'] as String? ?? ''),
tokenContract: parseHexAddress(
formValues['tokenContract'] as String? ?? '',
),
target: targetText.trim().isEmpty ? null : parseHexAddress(targetText),
volumeLimits: limits
.where((e) => e.amount.trim().isNotEmpty && e.windowSeconds.trim().isNotEmpty)
.where(
(e) =>
e.amount.trim().isNotEmpty &&
e.windowSeconds.trim().isNotEmpty,
)
.map(
(e) => VolumeRateLimit(
maxVolume: parseBigIntBytes(e.amount),
@@ -153,9 +160,9 @@ class _TokenVolumeLimitsField extends StatelessWidget {
Expanded(
child: Text(
'Token volume limits',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800),
),
),
TextButton.icon(
@@ -196,7 +203,9 @@ class _TokenVolumeLimitRow extends HookWidget {
@override
Widget build(BuildContext context) {
final amountController = useTextEditingController(text: value.amount);
final windowController = useTextEditingController(text: value.windowSeconds);
final windowController = useTextEditingController(
text: value.windowSeconds,
);
return Row(
children: [
@@ -214,8 +223,7 @@ class _TokenVolumeLimitRow extends HookWidget {
Expanded(
child: TextField(
controller: windowController,
onChanged: (next) =>
onChanged(value.copyWith(windowSeconds: next)),
onChanged: (next) => onChanged(value.copyWith(windowSeconds: next)),
decoration: const InputDecoration(
labelText: 'Window (seconds)',
border: OutlineInputBorder(),

View File

@@ -25,10 +25,10 @@ const _etherHandler = EtherTransferGrantHandler();
const _tokenHandler = TokenTransferGrantHandler();
GrantFormHandler _handlerFor(SpecificGrant_Grant type) => switch (type) {
SpecificGrant_Grant.etherTransfer => _etherHandler,
SpecificGrant_Grant.tokenTransfer => _tokenHandler,
_ => throw ArgumentError('Unsupported grant type: $type'),
};
SpecificGrant_Grant.etherTransfer => _etherHandler,
SpecificGrant_Grant.tokenTransfer => _tokenHandler,
_ => throw ArgumentError('Unsupported grant type: $type'),
};
@RoutePage()
class CreateEvmGrantScreen extends HookConsumerWidget {
@@ -62,12 +62,14 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
);
final validFrom = formValues['validFrom'] as DateTime?;
final validUntil = formValues['validUntil'] as DateTime?;
if (validFrom != null) sharedSettings.validFrom = toTimestamp(validFrom);
if (validFrom != null)
sharedSettings.validFrom = toTimestamp(validFrom);
if (validUntil != null) {
sharedSettings.validUntil = toTimestamp(validUntil);
}
final gasBytes =
optionalBigIntBytes(formValues['maxGasFeePerGas'] as String? ?? '');
final gasBytes = optionalBigIntBytes(
formValues['maxGasFeePerGas'] as String? ?? '',
);
if (gasBytes != null) sharedSettings.maxGasFeePerGas = gasBytes;
final priorityBytes = optionalBigIntBytes(
formValues['maxPriorityFeePerGas'] as String? ?? '',
@@ -106,7 +108,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
SizedBox(height: 1.8.h),
const _Section(
title: 'Authorization',
tooltip: 'Select which SDK client receives this grant and '
tooltip:
'Select which SDK client receives this grant and '
'which of its wallet accesses it applies to.',
child: AuthorizationFields(),
),
@@ -118,7 +121,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Chain',
tooltip: 'Restrict this grant to a specific EVM chain ID. '
tooltip:
'Restrict this grant to a specific EVM chain ID. '
'Leave empty to allow any chain.',
optional: true,
child: ChainIdField(),
@@ -128,7 +132,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Timing',
tooltip: 'Set an optional validity window. '
tooltip:
'Set an optional validity window. '
'Signing requests outside this period will be rejected.',
optional: true,
child: ValidityWindowField(),
@@ -145,7 +150,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Gas limits',
tooltip: 'Cap the gas fees this grant may authorize. '
tooltip:
'Cap the gas fees this grant may authorize. '
'Transactions exceeding these values will be rejected.',
optional: true,
child: GasFeeOptionsField(),
@@ -155,7 +161,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Transaction limits',
tooltip: 'Limit how many transactions can be signed '
tooltip:
'Limit how many transactions can be signed '
'within a rolling time window.',
optional: true,
child: TransactionRateLimitField(),
@@ -172,7 +179,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
SizedBox(height: 1.8.h),
_Section(
title: 'Grant-specific options',
tooltip: 'Rules specific to the selected transfer type. '
tooltip:
'Rules specific to the selected transfer type. '
'Switch between Ether and token above to change these fields.',
child: handler.buildForm(context, ref),
),
@@ -180,8 +188,7 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
onPressed:
createMutation is MutationPending ? null : submit,
onPressed: createMutation is MutationPending ? null : submit,
icon: createMutation is MutationPending
? SizedBox(
width: 1.8.h,
@@ -266,9 +273,9 @@ class _Section extends StatelessWidget {
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800),
),
SizedBox(width: 0.4.w),
Tooltip(
@@ -283,9 +290,9 @@ class _Section extends StatelessWidget {
SizedBox(width: 0.6.w),
Text(
'(optional)',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: subtleColor,
),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: subtleColor),
),
],
],

View File

@@ -19,4 +19,3 @@ class AuthorizationFields extends StatelessWidget {
);
}
}

View File

@@ -46,9 +46,7 @@ class GrantCard extends ConsumerWidget {
final accessById = <int, ua_sdk.WalletAccessEntry>{
for (final a in walletAccesses) a.id: a,
};
final walletById = <int, WalletEntry>{
for (final w in wallets) w.id: w,
};
final walletById = <int, WalletEntry>{for (final w in wallets) w.id: w};
final clientNameById = <int, String>{
for (final c in clients) c.id: c.info.name,
};
@@ -192,8 +190,9 @@ class GrantCard extends ConsumerWidget {
padding: EdgeInsets.symmetric(horizontal: 0.8.w),
child: Text(
'·',
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
style: theme.textTheme.bodySmall?.copyWith(
color: muted,
),
),
),
Expanded(
@@ -201,8 +200,9 @@ class GrantCard extends ConsumerWidget {
clientLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
style: theme.textTheme.bodySmall?.copyWith(
color: muted,
),
),
),
],

View File

@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
class CreateWalletButton extends ConsumerWidget {
const CreateWalletButton({super.key});
@@ -88,7 +87,6 @@ class RefreshWalletButton extends ConsumerWidget {
}
}
String _formatError(Object error) {
final message = error.toString();
if (message.startsWith('Exception: ')) {

View File

@@ -36,54 +36,51 @@ class WalletTable extends StatelessWidget {
return CreamFrame(
padding: EdgeInsets.all(2.h),
child: LayoutBuilder(
builder: (context, constraints) {
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
builder: (context, constraints) {
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Managed wallets',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Managed wallets',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
SizedBox(height: 0.6.h),
Text(
'Every address here is generated and held by Arbiter.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
),
SizedBox(height: 0.6.h),
Text(
'Every address here is generated and held by Arbiter.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _WalletTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < wallets.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == wallets.length - 1 ? 0 : 1.h,
),
child: _WalletTableRow(
wallet: wallets[i],
index: i,
),
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _WalletTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < wallets.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == wallets.length - 1 ? 0 : 1.h,
),
],
),
child: _WalletTableRow(wallet: wallets[i], index: i),
),
],
),
),
],
);
},
),
),
],
);
},
),
);
}
}

View File

@@ -0,0 +1,22 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MldsaKey>>
abstract class MldsaKey implements RustOpaqueInterface {
static Future<MldsaKey> fromBytes({required List<int> bytes}) =>
RustLib.instance.api.crateApiMldsaKeyFromBytes(bytes: bytes);
static Future<MldsaKey> generate() =>
RustLib.instance.api.crateApiMldsaKeyGenerate();
Future<Uint8List> getPublicKey();
Future<Uint8List> sign({required List<int> message});
Future<Uint8List> toBytes();
}

View File

@@ -0,0 +1,573 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'api.dart';
import 'dart:async';
import 'dart:convert';
import 'frb_generated.dart';
import 'frb_generated.io.dart'
if (dart.library.js_interop) 'frb_generated.web.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
/// Main entrypoint of the Rust API
class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
@internal
static final instance = RustLib._();
RustLib._();
/// Initialize flutter_rust_bridge
static Future<void> init({
RustLibApi? api,
BaseHandler? handler,
ExternalLibrary? externalLibrary,
bool forceSameCodegenVersion = true,
}) async {
await instance.initImpl(
api: api,
handler: handler,
externalLibrary: externalLibrary,
forceSameCodegenVersion: forceSameCodegenVersion,
);
}
/// Initialize flutter_rust_bridge in mock mode.
/// No libraries for FFI are loaded.
static void initMock({required RustLibApi api}) {
instance.initMockImpl(api: api);
}
/// Dispose flutter_rust_bridge
///
/// The call to this function is optional, since flutter_rust_bridge (and everything else)
/// is automatically disposed when the app stops.
static void dispose() => instance.disposeImpl();
@override
ApiImplConstructor<RustLibApiImpl, RustLibWire> get apiImplConstructor =>
RustLibApiImpl.new;
@override
WireConstructor<RustLibWire> get wireConstructor =>
RustLibWire.fromExternalLibrary;
@override
Future<void> executeRustInitializers() async {}
@override
ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig =>
kDefaultExternalLibraryLoaderConfig;
@override
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => -437661335;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
stem: 'rust_lib_arbiter',
ioDirectory: 'rust/target/release/',
webPrefix: 'pkg/',
wasmBindgenName: 'wasm_bindgen',
);
}
abstract class RustLibApi extends BaseApi {
Future<MldsaKey> crateApiMldsaKeyFromBytes({required List<int> bytes});
Future<MldsaKey> crateApiMldsaKeyGenerate();
Future<Uint8List> crateApiMldsaKeyGetPublicKey({required MldsaKey that});
Future<Uint8List> crateApiMldsaKeySign({
required MldsaKey that,
required List<int> message,
});
Future<Uint8List> crateApiMldsaKeyToBytes({required MldsaKey that});
RustArcIncrementStrongCountFnType
get rust_arc_increment_strong_count_MldsaKey;
RustArcDecrementStrongCountFnType
get rust_arc_decrement_strong_count_MldsaKey;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_MldsaKeyPtr;
}
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
RustLibApiImpl({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@override
Future<MldsaKey> crateApiMldsaKeyFromBytes({required List<int> bytes}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_list_prim_u_8_loose(bytes, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 1,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData:
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateApiMldsaKeyFromBytesConstMeta,
argValues: [bytes],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMldsaKeyFromBytesConstMeta => const TaskConstMeta(
debugName: "MldsaKey_from_bytes",
argNames: ["bytes"],
);
@override
Future<MldsaKey> crateApiMldsaKeyGenerate() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 2,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData:
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey,
decodeErrorData: null,
),
constMeta: kCrateApiMldsaKeyGenerateConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMldsaKeyGenerateConstMeta =>
const TaskConstMeta(debugName: "MldsaKey_generate", argNames: []);
@override
Future<Uint8List> crateApiMldsaKeyGetPublicKey({required MldsaKey that}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
that,
serializer,
);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 3,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: null,
),
constMeta: kCrateApiMldsaKeyGetPublicKeyConstMeta,
argValues: [that],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMldsaKeyGetPublicKeyConstMeta =>
const TaskConstMeta(
debugName: "MldsaKey_get_public_key",
argNames: ["that"],
);
@override
Future<Uint8List> crateApiMldsaKeySign({
required MldsaKey that,
required List<int> message,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
that,
serializer,
);
sse_encode_list_prim_u_8_loose(message, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 4,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateApiMldsaKeySignConstMeta,
argValues: [that, message],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMldsaKeySignConstMeta => const TaskConstMeta(
debugName: "MldsaKey_sign",
argNames: ["that", "message"],
);
@override
Future<Uint8List> crateApiMldsaKeyToBytes({required MldsaKey that}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
that,
serializer,
);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 5,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: null,
),
constMeta: kCrateApiMldsaKeyToBytesConstMeta,
argValues: [that],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMldsaKeyToBytesConstMeta =>
const TaskConstMeta(debugName: "MldsaKey_to_bytes", argNames: ["that"]);
RustArcIncrementStrongCountFnType
get rust_arc_increment_strong_count_MldsaKey => wire
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey;
RustArcDecrementStrongCountFnType
get rust_arc_decrement_strong_count_MldsaKey => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return AnyhowException(raw as String);
}
@protected
MldsaKey
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return MldsaKeyImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
MldsaKey
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return MldsaKeyImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
MldsaKey
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return MldsaKeyImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
String dco_decode_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as String;
}
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as List<int>;
}
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as Uint8List;
}
@protected
int dco_decode_u_8(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as int;
}
@protected
void dco_decode_unit(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return;
}
@protected
BigInt dco_decode_usize(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dcoDecodeU64(raw);
}
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_String(deserializer);
return AnyhowException(inner);
}
@protected
MldsaKey
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return MldsaKeyImpl.frbInternalSseDecode(
sse_decode_usize(deserializer),
sse_decode_i_32(deserializer),
);
}
@protected
MldsaKey
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return MldsaKeyImpl.frbInternalSseDecode(
sse_decode_usize(deserializer),
sse_decode_i_32(deserializer),
);
}
@protected
MldsaKey
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return MldsaKeyImpl.frbInternalSseDecode(
sse_decode_usize(deserializer),
sse_decode_i_32(deserializer),
);
}
@protected
String sse_decode_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_list_prim_u_8_strict(deserializer);
return utf8.decoder.convert(inner);
}
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var len_ = sse_decode_i_32(deserializer);
return deserializer.buffer.getUint8List(len_);
}
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var len_ = sse_decode_i_32(deserializer);
return deserializer.buffer.getUint8List(len_);
}
@protected
int sse_decode_u_8(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getUint8();
}
@protected
void sse_decode_unit(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
}
@protected
BigInt sse_decode_usize(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getBigUint64();
}
@protected
int sse_decode_i_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getInt32();
}
@protected
bool sse_decode_bool(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getUint8() != 0;
}
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.message, serializer);
}
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_usize(
(self as MldsaKeyImpl).frbInternalSseEncode(move: true),
serializer,
);
}
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_usize(
(self as MldsaKeyImpl).frbInternalSseEncode(move: false),
serializer,
);
}
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_usize(
(self as MldsaKeyImpl).frbInternalSseEncode(move: null),
serializer,
);
}
@protected
void sse_encode_String(String self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer);
}
@protected
void sse_encode_list_prim_u_8_loose(
List<int> self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.length, serializer);
serializer.buffer.putUint8List(
self is Uint8List ? self : Uint8List.fromList(self),
);
}
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.length, serializer);
serializer.buffer.putUint8List(self);
}
@protected
void sse_encode_u_8(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putUint8(self);
}
@protected
void sse_encode_unit(void self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
}
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putBigUint64(self);
}
@protected
void sse_encode_i_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putInt32(self);
}
@protected
void sse_encode_bool(bool self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putUint8(self ? 1 : 0);
}
}
@sealed
class MldsaKeyImpl extends RustOpaque implements MldsaKey {
// Not to be used by end users
MldsaKeyImpl.frbInternalDcoDecode(List<dynamic> wire)
: super.frbInternalDcoDecode(wire, _kStaticData);
// Not to be used by end users
MldsaKeyImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative)
: super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData);
static final _kStaticData = RustArcStaticData(
rustArcIncrementStrongCount:
RustLib.instance.api.rust_arc_increment_strong_count_MldsaKey,
rustArcDecrementStrongCount:
RustLib.instance.api.rust_arc_decrement_strong_count_MldsaKey,
rustArcDecrementStrongCountPtr:
RustLib.instance.api.rust_arc_decrement_strong_count_MldsaKeyPtr,
);
Future<Uint8List> getPublicKey() =>
RustLib.instance.api.crateApiMldsaKeyGetPublicKey(that: this);
Future<Uint8List> sign({required List<int> message}) =>
RustLib.instance.api.crateApiMldsaKeySign(that: this, message: message);
Future<Uint8List> toBytes() =>
RustLib.instance.api.crateApiMldsaKeyToBytes(that: this);
}

View File

@@ -0,0 +1,211 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'api.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_MldsaKeyPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
MldsaKey
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
);
@protected
MldsaKey
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
);
@protected
MldsaKey
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
);
@protected
String dco_decode_String(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
MldsaKey
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
);
@protected
MldsaKey
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
);
@protected
MldsaKey
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
RustLibWire(lib.ffiDynamicLibrary);
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_arbiter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_arbiter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
}

View File

@@ -0,0 +1,203 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
// Static analysis wrongly picks the IO variant, thus ignore this
// ignore_for_file: argument_type_not_assignable
import 'api.dart';
import 'dart:async';
import 'dart:convert';
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_MldsaKeyPtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
MldsaKey
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
);
@protected
MldsaKey
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
);
@protected
MldsaKey
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
dynamic raw,
);
@protected
String dco_decode_String(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
MldsaKey
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
);
@protected
MldsaKey
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
);
@protected
MldsaKey
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
MldsaKey self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
int ptr,
) => wasmModule
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
ptr,
);
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
int ptr,
) => wasmModule
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
ptr,
);
}
@JS('wasm_bindgen')
external RustLibWasmModule get wasmModule;
@JS()
@anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject {
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
int ptr,
);
external void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey(
int ptr,
);
}

View File

@@ -50,7 +50,9 @@ class _BottomPopupRoute extends StatelessWidget {
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: barrierDismissible ? () => Navigator.of(context).pop() : null,
onTap: barrierDismissible
? () => Navigator.of(context).pop()
: null,
child: AnimatedBuilder(
animation: barrierAnimation,
builder: (context, child) {
@@ -71,11 +73,10 @@ class _BottomPopupRoute extends StatelessWidget {
child: FadeTransition(
opacity: popupAnimation,
child: SlideTransition(
position:
Tween<Offset>(
begin: const Offset(0, 0.08),
end: Offset.zero,
).animate(popupAnimation),
position: Tween<Offset>(
begin: const Offset(0, 0.08),
end: Offset.zero,
).animate(popupAnimation),
child: GestureDetector(
onTap: () {},
child: Builder(builder: builder),

View File

@@ -28,42 +28,42 @@ class StatePanel extends StatelessWidget {
return CreamFrame(
padding: EdgeInsets.all(2.8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (busy)
SizedBox(
width: 2.8.h,
height: 2.8.h,
child: const CircularProgressIndicator(strokeWidth: 2.5),
)
else
Icon(icon, size: 34, color: Palette.coral),
SizedBox(height: 1.8.h),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (busy)
SizedBox(
width: 2.8.h,
height: 2.8.h,
child: const CircularProgressIndicator(strokeWidth: 2.5),
)
else
Icon(icon, size: 34, color: Palette.coral),
SizedBox(height: 1.8.h),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
SizedBox(height: 1.h),
Text(
body,
style: theme.textTheme.bodyLarge?.copyWith(
color: Palette.ink.withValues(alpha: 0.72),
height: 1.5,
),
),
SizedBox(height: 1.h),
Text(
body,
style: theme.textTheme.bodyLarge?.copyWith(
color: Palette.ink.withValues(alpha: 0.72),
height: 1.5,
),
),
if (actionLabel != null && onAction != null) ...[
SizedBox(height: 2.h),
OutlinedButton.icon(
onPressed: () => onAction!(),
icon: const Icon(Icons.refresh),
label: Text(actionLabel!),
),
if (actionLabel != null && onAction != null) ...[
SizedBox(height: 2.h),
OutlinedButton.icon(
onPressed: () => onAction!(),
icon: const Icon(Icons.refresh),
label: Text(actionLabel!),
),
],
],
),
],
),
);
}
}