diff --git a/assets/fonts/Roboto-Light.ttf b/assets/fonts/Roboto-Light.ttf new file mode 100644 index 00000000..0e977514 Binary files /dev/null and b/assets/fonts/Roboto-Light.ttf differ diff --git a/assets/fonts/Roboto-Regular.ttf b/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 00000000..3d6861b4 Binary files /dev/null and b/assets/fonts/Roboto-Regular.ttf differ diff --git a/assets/fonts/Roboto-Thin.ttf b/assets/fonts/Roboto-Thin.ttf new file mode 100644 index 00000000..7d084aed Binary files /dev/null and b/assets/fonts/Roboto-Thin.ttf differ diff --git a/assets/graphics/no-accounts.png b/assets/graphics/no-accounts.png new file mode 100644 index 00000000..fa267762 Binary files /dev/null and b/assets/graphics/no-accounts.png differ diff --git a/assets/graphics/no-discoverable.png b/assets/graphics/no-discoverable.png new file mode 100644 index 00000000..db7e81e9 Binary files /dev/null and b/assets/graphics/no-discoverable.png differ diff --git a/assets/graphics/no-fingerprints.png b/assets/graphics/no-fingerprints.png new file mode 100644 index 00000000..1be18d4b Binary files /dev/null and b/assets/graphics/no-fingerprints.png differ diff --git a/assets/graphics/no-permission.png b/assets/graphics/no-permission.png new file mode 100755 index 00000000..30c32aa3 Binary files /dev/null and b/assets/graphics/no-permission.png differ diff --git a/assets/product-images/neo.png b/assets/product-images/neo.png index 044eddc2..9385f4dc 100644 Binary files a/assets/product-images/neo.png and b/assets/product-images/neo.png differ diff --git a/assets/product-images/sky1.png b/assets/product-images/sky1.png index a5c6096d..59e88eb3 100644 Binary files a/assets/product-images/sky1.png and b/assets/product-images/sky1.png differ diff --git a/assets/product-images/sky2.png b/assets/product-images/sky2.png index 6d95b3c2..b65719ff 100644 Binary files a/assets/product-images/sky2.png and b/assets/product-images/sky2.png differ diff --git a/assets/product-images/sky3.png b/assets/product-images/sky3.png index 2f591352..84337918 100644 Binary files a/assets/product-images/sky3.png and b/assets/product-images/sky3.png differ diff --git a/assets/product-images/skycnfc.png b/assets/product-images/skycnfc.png index 359b6a15..817da1f8 100644 Binary files a/assets/product-images/skycnfc.png and b/assets/product-images/skycnfc.png differ diff --git a/assets/product-images/standard.png b/assets/product-images/standard.png index 055ff26b..7664130d 100644 Binary files a/assets/product-images/standard.png and b/assets/product-images/standard.png differ diff --git a/assets/product-images/yk4.png b/assets/product-images/yk4.png index 58419072..fc378dd7 100644 Binary files a/assets/product-images/yk4.png and b/assets/product-images/yk4.png differ diff --git a/assets/product-images/yk4series.png b/assets/product-images/yk4series.png index 7164ed6e..6dee42ea 100644 Binary files a/assets/product-images/yk4series.png and b/assets/product-images/yk4series.png differ diff --git a/assets/product-images/yk5c.png b/assets/product-images/yk5c.png index a995e465..bfef9fd0 100644 Binary files a/assets/product-images/yk5c.png and b/assets/product-images/yk5c.png differ diff --git a/assets/product-images/yk5ci.png b/assets/product-images/yk5ci.png index 9a2b3463..70bca7f3 100644 Binary files a/assets/product-images/yk5ci.png and b/assets/product-images/yk5ci.png differ diff --git a/assets/product-images/yk5cnano.png b/assets/product-images/yk5cnano.png index 308e5484..844f241c 100644 Binary files a/assets/product-images/yk5cnano.png and b/assets/product-images/yk5cnano.png differ diff --git a/assets/product-images/yk5cnfc.png b/assets/product-images/yk5cnfc.png index 9d8121a9..f26a8ab5 100644 Binary files a/assets/product-images/yk5cnfc.png and b/assets/product-images/yk5cnfc.png differ diff --git a/assets/product-images/yk5nano.png b/assets/product-images/yk5nano.png index 4526b316..3faf5077 100644 Binary files a/assets/product-images/yk5nano.png and b/assets/product-images/yk5nano.png differ diff --git a/assets/product-images/yk5nfc.png b/assets/product-images/yk5nfc.png index c9fe656d..45e7cfde 100644 Binary files a/assets/product-images/yk5nfc.png and b/assets/product-images/yk5nfc.png differ diff --git a/assets/product-images/yk5series.png b/assets/product-images/yk5series.png index 04a627d2..f4ca1ddc 100644 Binary files a/assets/product-images/yk5series.png and b/assets/product-images/yk5series.png differ diff --git a/assets/product-images/ykbioa.png b/assets/product-images/ykbioa.png index ca7d795f..49195225 100644 Binary files a/assets/product-images/ykbioa.png and b/assets/product-images/ykbioa.png differ diff --git a/assets/product-images/ykbioc.png b/assets/product-images/ykbioc.png index 60896380..2be0b21e 100644 Binary files a/assets/product-images/ykbioc.png and b/assets/product-images/ykbioc.png differ diff --git a/assets/product-images/ykedge.png b/assets/product-images/ykedge.png index 797bdd3f..614f6c02 100644 Binary files a/assets/product-images/ykedge.png and b/assets/product-images/ykedge.png differ diff --git a/assets/product-images/ykplus.png b/assets/product-images/ykplus.png index 49d32215..6298dad7 100644 Binary files a/assets/product-images/ykplus.png and b/assets/product-images/ykplus.png differ diff --git a/helper/helper/device.py b/helper/helper/device.py index 179c57d2..284a90cb 100644 --- a/helper/helper/device.py +++ b/helper/helper/device.py @@ -26,7 +26,14 @@ # POSSIBILITY OF SUCH DAMAGE. -from .base import RpcNode, child, action, NoSuchNodeException, ChildResetException +from .base import ( + RpcNode, + child, + action, + RpcException, + NoSuchNodeException, + ChildResetException, +) from .oath import OathNode from .fido import Ctap2Node from .yubiotp import YubiOtpNode @@ -47,6 +54,7 @@ from yubikit.logging import LOG_LEVEL from ykman.pcsc import list_devices, YK_READER_NAME from smartcard.Exceptions import SmartcardException +from smartcard.pcsc.PCSCExceptions import EstablishContextException from hashlib import sha256 from dataclasses import asdict from typing import Mapping, Tuple @@ -65,6 +73,15 @@ def _is_admin(): return os.getuid() == 0 +class ConnectionException(RpcException): + def __init__(self, connection, exc_type): + super().__init__( + "connection-error", + f"Error connecting to {connection} interface", + dict(connection=connection, exc_type=type(exc_type).__name__), + ) + + class RootNode(RpcNode): def __init__(self): super().__init__() @@ -127,9 +144,16 @@ class ReadersNode(RpcNode): return self.list_children() def list_children(self): - devices = [ - d for d in list_devices("") if YK_READER_NAME not in d.reader.name.lower() - ] + try: + devices = [ + d + for d in list_devices("") + if YK_READER_NAME not in d.reader.name.lower() + ] + except EstablishContextException: + logger.warning("Unable to list readers", exc_info=True) + return {} + state = {d.reader.name for d in devices} if self._state != state: self._readers = {} @@ -271,15 +295,27 @@ class UsbDeviceNode(AbstractDeviceNode): @child(condition=lambda self: self._supports_connection(SmartCardConnection)) def ccid(self): - return self._create_connection(SmartCardConnection) + try: + return self._create_connection(SmartCardConnection) + except (ValueError, SmartcardException) as e: + logger.warning("Error opening connection", exc_info=True) + raise ConnectionException("ccid", e) @child(condition=lambda self: self._supports_connection(OtpConnection)) def otp(self): - return self._create_connection(OtpConnection) + try: + return self._create_connection(OtpConnection) + except (ValueError, OSError) as e: + logger.warning("Error opening connection", exc_info=True) + raise ConnectionException("otp", e) @child(condition=lambda self: self._supports_connection(FidoConnection)) def fido(self): - return self._create_connection(FidoConnection) + try: + return self._create_connection(FidoConnection) + except (ValueError, OSError) as e: + logger.warning("Error opening connection", exc_info=True) + raise ConnectionException("fido", e) class ReaderDeviceNode(AbstractDeviceNode): diff --git a/lib/android/qr_scanner/qr_scanner_view.dart b/lib/android/qr_scanner/qr_scanner_view.dart index cfafd989..1215e24b 100755 --- a/lib/android/qr_scanner/qr_scanner_view.dart +++ b/lib/android/qr_scanner/qr_scanner_view.dart @@ -185,19 +185,19 @@ class _QrScannerViewState extends State { Text('Looking for a code...', style: Theme.of(context) .textTheme - .headline6 + .titleLarge ?.copyWith(color: Colors.black)), if (_status == _ScanStatus.success) Text('Found a valid code', style: Theme.of(context) .textTheme - .headline6 + .titleLarge ?.copyWith(color: Colors.white)), if (_status == _ScanStatus.error) Text('This code is not valid, try again.', style: Theme.of(context) .textTheme - .headline6 + .titleLarge ?.copyWith(color: Colors.white)), ]), Row( diff --git a/lib/app/views/app_failure_page.dart b/lib/app/views/app_failure_page.dart new file mode 100755 index 00000000..aa4120eb --- /dev/null +++ b/lib/app/views/app_failure_page.dart @@ -0,0 +1,83 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../desktop/models.dart'; +import '../../desktop/state.dart'; +import '../../theme.dart'; +import '../message.dart'; +import 'graphics.dart'; +import 'message_page.dart'; + +class AppFailurePage extends ConsumerWidget { + final Widget? title; + final Object cause; + const AppFailurePage({this.title, required this.cause, super.key}) : super(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final reason = cause; + + Widget? graphic = const Icon(Icons.error); + String? header = 'An error has occured'; + String? message = reason.toString(); + List actions = []; + + if (reason is RpcError) { + if (reason.status == 'connection-error') { + switch (reason.body['connection']) { + case 'ccid': + header = 'Failed to open smart card connection'; + if (Platform.isMacOS) { + message = 'Try to remove and re-insert your YubiKey.'; + } else if (Platform.isLinux) { + message = 'Make sure pcscd is running.'; + } else { + message = 'Make sure your smart card service is functioning.'; + } + break; + case 'fido': + if (Platform.isWindows && + !ref.watch(rpcStateProvider.select((state) => state.isAdmin))) { + graphic = noPermission; + header = null; + message = 'WebAuthn management requires elevated privileges.'; + actions = [ + OutlinedButton.icon( + label: const Text('Unlock'), + icon: const Icon(Icons.lock_open), + style: AppTheme.primaryOutlinedButtonStyle(context), + onPressed: () async { + final controller = showMessage( + context, 'Elevating permissions...', + duration: const Duration(seconds: 30)); + try { + if (await ref.read(rpcProvider).elevate()) { + ref.refresh(rpcProvider); + } else { + showMessage(context, 'Permission denied'); + } + } finally { + controller.close(); + } + }), + ]; + } + break; + default: + header = 'Failed to open connection'; + message = 'Try to remove and re-insert your YubiKey.'; + } + } + } + + return MessagePage( + title: title, + graphic: graphic, + header: header, + message: message, + actions: actions, + ); + } +} diff --git a/lib/app/views/app_failure_screen.dart b/lib/app/views/app_failure_screen.dart deleted file mode 100755 index 3f9bd484..00000000 --- a/lib/app/views/app_failure_screen.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFailureScreen extends StatelessWidget { - final String reason; - const AppFailureScreen(this.reason, {super.key}) : super(); - - @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - reason, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index f0540a6d..ec5f32cf 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -8,14 +8,15 @@ class AppPage extends ConsumerWidget { final Key _scaffoldKey = GlobalKey(); final Widget? title; final Widget child; - final Widget? floatingActionButton; + final List actions; final bool centered; - AppPage( - {super.key, - this.title, - required this.child, - this.floatingActionButton, - this.centered = false}); + AppPage({ + super.key, + this.title, + required this.child, + this.actions = const [], + this.centered = false, + }); @override Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( @@ -29,7 +30,7 @@ class AppPage extends ConsumerWidget { body: Row( children: [ const SizedBox( - width: 240, + width: 280, child: ListTileTheme( style: ListTileStyle.drawer, child: MainPageDrawer(shouldPop: false)), @@ -46,11 +47,26 @@ class AppPage extends ConsumerWidget { Widget _buildScrollView() => SafeArea( child: SingleChildScrollView( - // Make sure FAB doesn't block content - padding: floatingActionButton != null - ? const EdgeInsets.only(bottom: 72) - : null, - child: child, + child: Builder(builder: (context) { + return Column( + children: [ + child, + if (actions.isNotEmpty) + Align( + alignment: + centered ? Alignment.center : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: actions, + ), + ), + ), + ], + ); + }), ), ); @@ -58,13 +74,14 @@ class AppPage extends ConsumerWidget { return Scaffold( key: _scaffoldKey, appBar: AppBar( + titleSpacing: 8, title: title, centerTitle: true, + titleTextStyle: Theme.of(context).textTheme.titleLarge, actions: const [DeviceButton()], ), drawer: hasDrawer ? const MainPageDrawer() : null, body: centered ? Center(child: _buildScrollView()) : _buildScrollView(), - floatingActionButton: floatingActionButton, ); } } diff --git a/lib/app/views/device_avatar.dart b/lib/app/views/device_avatar.dart index 9b122296..9dd0b81a 100755 --- a/lib/app/views/device_avatar.dart +++ b/lib/app/views/device_avatar.dart @@ -46,7 +46,7 @@ class DeviceAvatar extends StatelessWidget { CircleAvatar( radius: 22, backgroundColor: selected - ? Theme.of(context).colorScheme.secondary + ? Theme.of(context).colorScheme.primary : Colors.transparent, child: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.background, diff --git a/lib/app/views/device_button.dart b/lib/app/views/device_button.dart index 513da16f..2916d551 100755 --- a/lib/app/views/device_button.dart +++ b/lib/app/views/device_button.dart @@ -33,6 +33,7 @@ class DeviceButton extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(right: 8.0), child: IconButton( + tooltip: 'Select YubiKey or device', icon: OverflowBox( maxHeight: 44, maxWidth: 44, diff --git a/lib/app/views/graphics.dart b/lib/app/views/graphics.dart new file mode 100755 index 00000000..bfe3c5f4 --- /dev/null +++ b/lib/app/views/graphics.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +final Image noAccounts = _graphic('no-accounts'); +final Image noDiscoverable = _graphic('no-discoverable'); +final Image noFingerprints = _graphic('no-fingerprints'); +final Image noPermission = _graphic('no-permission'); + +Image _graphic(String name) => Image.asset('assets/graphics/$name.png'); diff --git a/lib/app/views/main_drawer.dart b/lib/app/views/main_drawer.dart index e09ea780..265e58b0 100755 --- a/lib/app/views/main_drawer.dart +++ b/lib/app/views/main_drawer.dart @@ -38,17 +38,20 @@ class MainPageDrawer extends ConsumerWidget { final data = ref.watch(currentDeviceDataProvider); final currentApp = ref.watch(currentAppProvider); + MediaQuery? mediaQuery = + context.findAncestorWidgetOfExactType(); + final width = mediaQuery?.data.size.width ?? 400; + return Drawer( + width: width < 357 ? 0.85 * width : null, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20.0), + bottomRight: Radius.circular(20.0))), child: ListView( primary: false, //Prevents conflict with the MainPage scroll view. children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - 'Yubico Authenticator', - style: Theme.of(context).textTheme.headline6, - ), - ), + const SizedBox(height: 24.0), if (data != null) ...[ // Normal YubiKey Applications ...supportedApps @@ -68,14 +71,6 @@ class MainPageDrawer extends ConsumerWidget { if (supportedApps.contains(Application.management) && Application.management.getAvailability(data) == Availability.enabled) ...[ - const Divider(), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Configuration', - style: Theme.of(context).textTheme.bodyText2, - ), - ), DrawerItem( titleText: 'Toggle applications', icon: Icon(Application.management._icon), @@ -87,17 +82,10 @@ class MainPageDrawer extends ConsumerWidget { ); }, ), - const Divider(), ], + const Divider(indent: 16.0, endIndent: 28.0), ], // Non-YubiKey pages - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Application', - style: Theme.of(context).textTheme.bodyText2, - ), - ), DrawerItem( titleText: 'Settings', icon: const Icon(Icons.settings), @@ -110,7 +98,7 @@ class MainPageDrawer extends ConsumerWidget { ), DrawerItem( titleText: 'Help and feedback', - icon: const Icon(Icons.help_outline), + icon: const Icon(Icons.help), onTap: () { final nav = Navigator.of(context); if (shouldPop) nav.pop(); @@ -173,17 +161,22 @@ class DrawerItem extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.only(left: 12.0, right: 12.0), child: ListTile( enabled: enabled, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.horizontal(right: Radius.circular(20)), + borderRadius: BorderRadius.all(Radius.circular(30)), ), dense: true, + minLeadingWidth: 24, + minVerticalPadding: 18, selected: selected, - selectedColor: Theme.of(context).backgroundColor, - selectedTileColor: Theme.of(context).colorScheme.secondary, - leading: icon, + selectedColor: Theme.of(context).colorScheme.onPrimary, + selectedTileColor: Theme.of(context).colorScheme.primary, + leading: IconTheme.merge( + data: const IconThemeData(size: 24), + child: icon, + ), title: Text(titleText), onTap: onTap, ), diff --git a/lib/app/views/message_page.dart b/lib/app/views/message_page.dart index cb5c2c18..3f2d766b 100755 --- a/lib/app/views/message_page.dart +++ b/lib/app/views/message_page.dart @@ -4,29 +4,40 @@ import 'app_page.dart'; class MessagePage extends StatelessWidget { final Widget? title; - final String header; - final String message; - final Widget? floatingActionButton; + final Widget? graphic; + final String? header; + final String? message; + final List actions; const MessagePage({ super.key, this.title, - required this.header, - required this.message, - this.floatingActionButton, + this.graphic, + this.header, + this.message, + this.actions = const [], }); @override Widget build(BuildContext context) => AppPage( title: title, centered: true, - floatingActionButton: floatingActionButton, - child: Column( - children: [ - Text(header, style: Theme.of(context).textTheme.headline6), - const SizedBox(height: 12.0), - Text(message, textAlign: TextAlign.center), - ], + actions: actions, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + if (graphic != null) graphic!, + if (header != null) + Text(header!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 12.0), + if (message != null) ...[ + Text(message!, textAlign: TextAlign.center), + ], + ], + ), ), ); } diff --git a/lib/app/views/no_device_screen.dart b/lib/app/views/no_device_screen.dart index a0aa2343..4277c009 100755 --- a/lib/app/views/no_device_screen.dart +++ b/lib/app/views/no_device_screen.dart @@ -5,25 +5,29 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/models.dart'; import '../../desktop/state.dart'; +import '../../theme.dart'; import '../message.dart'; import '../models.dart'; -import 'app_page.dart'; import 'device_avatar.dart'; +import 'graphics.dart'; +import 'message_page.dart'; class NoDeviceScreen extends ConsumerWidget { final DeviceNode? node; const NoDeviceScreen(this.node, {super.key}); - List _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) { + Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) { if (pid.usbInterfaces == UsbInterface.fido.value) { if (Platform.isWindows && !ref.watch(rpcStateProvider.select((state) => state.isAdmin))) { - return [ - const DeviceAvatar(child: Icon(Icons.lock)), - const Text('WebAuthn management requires elevated privileges.'), - OutlinedButton.icon( - icon: const Icon(Icons.lock_open), + return MessagePage( + graphic: noPermission, + message: 'Managing this device requires elevated privileges.', + actions: [ + OutlinedButton.icon( + style: AppTheme.primaryOutlinedButtonStyle(context), label: const Text('Unlock'), + icon: const Icon(Icons.lock_open), onPressed: () async { final controller = showMessage( context, 'Elevating permissions...', @@ -37,43 +41,31 @@ class NoDeviceScreen extends ConsumerWidget { } finally { controller.close(); } - }), - ] - .map((e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: e, - )) - .toList(); + }, + ), + ], + ); } } - return [ - const DeviceAvatar(child: Icon(Icons.usb_off)), - const Text( - 'This YubiKey cannot be accessed', - textAlign: TextAlign.center, - ), - ]; + return const MessagePage( + graphic: DeviceAvatar(child: Icon(Icons.usb_off)), + message: 'This YubiKey cannot be accessed', + ); } @override Widget build(BuildContext context, WidgetRef ref) { - return AppPage( - centered: true, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: node?.map(usbYubiKey: (node) { - return _buildUsbPid(context, ref, node.pid); - }, nfcReader: (node) { - return const [ - DeviceAvatar(child: Icon(Icons.wifi)), - Text('Place your YubiKey on the NFC reader'), - ]; - }) ?? - const [ - DeviceAvatar(child: Icon(Icons.usb)), - Text('Insert your YubiKey'), - ], - ), - ); + return node?.map(usbYubiKey: (node) { + return _buildUsbPid(context, ref, node.pid); + }, nfcReader: (node) { + return const MessagePage( + graphic: DeviceAvatar(child: Icon(Icons.wifi)), + message: 'Place your YubiKey on the NFC reader', + ); + }) ?? + const MessagePage( + graphic: DeviceAvatar(child: Icon(Icons.usb)), + message: 'Insert your YubiKey', + ); } } diff --git a/lib/fido/views/add_fingerprint_dialog.dart b/lib/fido/views/add_fingerprint_dialog.dart index 05ecc99f..986f2627 100755 --- a/lib/fido/views/add_fingerprint_dialog.dart +++ b/lib/fido/views/add_fingerprint_dialog.dart @@ -133,26 +133,27 @@ class _AddFingerprintDialogState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Step 1/2: Capture fingerprint'), - Card( - child: Column( - children: [ - AnimatedBuilder( + Column( + children: [ + Padding( + padding: const EdgeInsets.all(36.0), + child: AnimatedBuilder( animation: _color, builder: (context, _) { return Icon( _fingerprint == null ? Icons.fingerprint : Icons.check, - size: 200.0, + size: 128.0, color: _color.value, ); }, ), - LinearProgressIndicator(value: progress), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text(_getMessage()), - ), - ], - ), + ), + LinearProgressIndicator(value: progress), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(_getMessage()), + ), + ], ), const Text('Step 2/2: Name fingerprint'), TextFormField( diff --git a/lib/fido/views/fido_screen.dart b/lib/fido/views/fido_screen.dart index 16a0ad8a..f9702398 100755 --- a/lib/fido/views/fido_screen.dart +++ b/lib/fido/views/fido_screen.dart @@ -1,16 +1,11 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../app/message.dart'; import '../../app/models.dart'; -import '../../app/views/app_failure_screen.dart'; +import '../../app/views/app_failure_page.dart'; import '../../app/views/app_loading_screen.dart'; import '../../app/views/app_page.dart'; -import '../../app/views/device_avatar.dart'; import '../../app/views/message_page.dart'; -import '../../desktop/state.dart'; import '../../management/models.dart'; import '../state.dart'; import 'locked_page.dart'; @@ -51,52 +46,10 @@ class FidoScreen extends ConsumerWidget { 'WebAuthn requires the FIDO2 application to be enabled on your YubiKey', ); } - if (Platform.isWindows) { - if (!ref - .watch(rpcStateProvider.select((state) => state.isAdmin))) { - return AppPage( - title: const Text('WebAuthn'), - centered: true, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const DeviceAvatar(child: Icon(Icons.lock)), - const Text( - 'WebAuthn management requires elevated privileges.', - textAlign: TextAlign.center, - ), - OutlinedButton.icon( - icon: const Icon(Icons.lock_open), - label: const Text('Unlock'), - onPressed: () async { - final controller = showMessage( - context, 'Elevating permissions...', - duration: const Duration(seconds: 30)); - try { - if (await ref.read(rpcProvider).elevate()) { - ref.refresh(rpcProvider); - } else { - showMessage(context, 'Permission denied'); - } - } finally { - controller.close(); - } - }), - ] - .map((e) => Padding( - padding: - const EdgeInsets.symmetric(vertical: 8.0), - child: e, - )) - .toList(), - )); - } - } - return AppPage( + + return AppFailurePage( title: const Text('WebAuthn'), - centered: true, - child: AppFailureScreen('$error'), + cause: error, ); }, data: (fidoState) { diff --git a/lib/fido/views/locked_page.dart b/lib/fido/views/locked_page.dart index 0cde0d6d..4ba735d2 100755 --- a/lib/fido/views/locked_page.dart +++ b/lib/fido/views/locked_page.dart @@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/views/app_page.dart'; +import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; +import '../../theme.dart'; import '../models.dart'; import '../state.dart'; import 'pin_dialog.dart'; @@ -22,23 +24,26 @@ class FidoLockedPage extends ConsumerWidget { if (state.bioEnroll != null) { return MessagePage( title: const Text('WebAuthn'), + graphic: noFingerprints, header: 'No fingerprints', - message: 'Set a PIN to register fingerprints', - floatingActionButton: _buildFab(context), + message: 'Set a PIN to register fingerprints.', + actions: _buildActions(context), ); } else { return MessagePage( title: const Text('WebAuthn'), + graphic: noDiscoverable, header: 'No discoverable accounts', message: 'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites', - floatingActionButton: _buildFab(context), + actions: _buildActions(context), ); } } return AppPage( title: const Text('WebAuthn'), + actions: _buildActions(context), child: Column( children: [ const ListTile(title: Text('Unlock')), @@ -48,41 +53,49 @@ class FidoLockedPage extends ConsumerWidget { ); } - FloatingActionButton _buildFab(BuildContext context) { - return FloatingActionButton.extended( - icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin), - label: const Text('Setup'), - onPressed: () { - showBottomMenu(context, [ - if (state.bioEnroll != null) - MenuAction( - text: 'Add fingerprint', - icon: const Icon(Icons.fingerprint), - ), - MenuAction( - text: 'Set PIN', - icon: const Icon(Icons.pin_outlined), - action: (context) { + List _buildActions(BuildContext context) => [ + if (!state.hasPin) + OutlinedButton.icon( + style: AppTheme.primaryOutlinedButtonStyle(context), + label: const Text('Set PIN'), + icon: const Icon(Icons.pin), + onPressed: () { showDialog( context: context, builder: (context) => FidoPinDialog(node.path, state), ); }, ), - MenuAction( - text: 'Reset FIDO', - icon: const Icon(Icons.delete_outline), - action: (context) { - showDialog( - context: context, - builder: (context) => ResetDialog(node), - ); - }, - ), - ]); - }, - ); - } + OutlinedButton.icon( + label: const Text('Options'), + icon: const Icon(Icons.tune), + onPressed: () { + showBottomMenu(context, [ + if (state.hasPin) + MenuAction( + text: 'Change PIN', + icon: const Icon(Icons.pin), + action: (context) { + showDialog( + context: context, + builder: (context) => FidoPinDialog(node.path, state), + ); + }, + ), + MenuAction( + text: 'Reset FIDO', + icon: const Icon(Icons.delete), + action: (context) { + showDialog( + context: context, + builder: (context) => ResetDialog(node), + ); + }, + ), + ]); + }, + ), + ]; } class _PinEntryForm extends ConsumerStatefulWidget { @@ -149,34 +162,6 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> { onSubmitted: (_) => _submit(), ), ), - Wrap( - spacing: 4.0, - runSpacing: 8.0, - children: [ - OutlinedButton.icon( - icon: const Icon(Icons.pin_outlined), - label: const Text('Change PIN'), - onPressed: () { - showDialog( - context: context, - builder: (context) => - FidoPinDialog(widget._deviceNode.path, widget._state), - ); - }, - ), - OutlinedButton.icon( - icon: const Icon(Icons.delete_outlined), - label: const Text('Reset FIDO'), - onPressed: () { - showDialog( - context: context, - builder: (context) => ResetDialog(widget._deviceNode), - ); - }, - ), - ], - ), - const SizedBox(height: 16.0), ListTile( leading: noFingerprints ? const Icon(Icons.warning_amber_rounded) : null, diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 24978d0d..6aa21c79 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -50,7 +50,7 @@ class _FidoPinDialogState extends ConsumerState { if (hasPin) ...[ Text( 'Current PIN', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), TextFormField( initialValue: _currentPin, @@ -70,7 +70,7 @@ class _FidoPinDialogState extends ConsumerState { ], Text( 'New PIN', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), TextFormField( initialValue: _newPin, diff --git a/lib/fido/views/reset_dialog.dart b/lib/fido/views/reset_dialog.dart index 49129572..f86b207d 100755 --- a/lib/fido/views/reset_dialog.dart +++ b/lib/fido/views/reset_dialog.dart @@ -94,7 +94,7 @@ class _ResetDialogState extends ConsumerState { ), Center( child: Text(_getMessage(), - style: Theme.of(context).textTheme.headline6), + style: Theme.of(context).textTheme.titleLarge), ), ] .map((e) => Padding( diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index 64b3ace6..7002f9e6 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/views/app_page.dart'; +import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; +import '../../theme.dart'; import '../models.dart'; import '../state.dart'; import 'add_fingerprint_dialog.dart'; @@ -29,10 +31,23 @@ class FidoUnlockedPage extends ConsumerWidget { ? [ const ListTile(title: Text('Credentials')), ...creds.map((cred) => ListTile( - leading: - const CircleAvatar(child: Icon(Icons.link)), - title: Text(cred.userName), - subtitle: Text(cred.rpId), + leading: CircleAvatar( + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + backgroundColor: + Theme.of(context).colorScheme.primary, + child: const Icon(Icons.person), + ), + title: Text( + cred.userName, + softWrap: false, + overflow: TextOverflow.fade, + ), + subtitle: Text( + cred.rpId, + softWrap: false, + overflow: TextOverflow.fade, + ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -45,7 +60,7 @@ class FidoUnlockedPage extends ConsumerWidget { node.path, cred), ); }, - icon: const Icon(Icons.delete_outlined)), + icon: const Icon(Icons.delete_outline)), ], ), )), @@ -59,9 +74,18 @@ class FidoUnlockedPage extends ConsumerWidget { ? [ const ListTile(title: Text('Fingerprints')), ...fingerprints.map((fp) => ListTile( - leading: const CircleAvatar( - child: Icon(Icons.fingerprint)), - title: Text(fp.label), + leading: CircleAvatar( + foregroundColor: + Theme.of(context).colorScheme.onSecondary, + backgroundColor: + Theme.of(context).colorScheme.secondary, + child: const Icon(Icons.fingerprint), + ), + title: Text( + fp.label, + softWrap: false, + overflow: TextOverflow.fade, + ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -74,7 +98,7 @@ class FidoUnlockedPage extends ConsumerWidget { node.path, fp), ); }, - icon: const Icon(Icons.edit)), + icon: const Icon(Icons.edit_outlined)), IconButton( onPressed: () { showDialog( @@ -84,7 +108,7 @@ class FidoUnlockedPage extends ConsumerWidget { node.path, fp), ); }, - icon: const Icon(Icons.delete_outlined)), + icon: const Icon(Icons.delete_outline)), ], ), )) @@ -97,69 +121,76 @@ class FidoUnlockedPage extends ConsumerWidget { if (children.isNotEmpty) { return AppPage( title: const Text('WebAuthn'), - floatingActionButton: _buildFab(context), + actions: _buildActions(context), child: Column( children: children, ), ); } - if (state.bioEnroll == false) { + if (state.bioEnroll != null) { return MessagePage( title: const Text('WebAuthn'), + graphic: noFingerprints, header: 'No fingerprints', message: 'Add one or more (up to five) fingerprints', - floatingActionButton: _buildFab(context), + actions: _buildActions(context, fingerprintPrimary: true), ); } return MessagePage( title: const Text('WebAuthn'), + graphic: noDiscoverable, header: 'No discoverable accounts', message: 'Register as a Security Key on websites', - floatingActionButton: _buildFab(context), + actions: _buildActions(context), ); } - FloatingActionButton _buildFab(BuildContext context) { - return FloatingActionButton.extended( - icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin), - label: const Text('Setup'), - onPressed: () { - showBottomMenu(context, [ - if (state.bioEnroll != null) - MenuAction( - text: 'Add fingerprint', - icon: const Icon(Icons.fingerprint), - action: (context) { - showDialog( - context: context, - builder: (context) => AddFingerprintDialog(node.path), - ); - }, - ), - MenuAction( - text: 'Change PIN', - icon: const Icon(Icons.pin_outlined), - action: (context) { + List _buildActions(BuildContext context, + {bool fingerprintPrimary = false}) => + [ + if (state.bioEnroll != null) + OutlinedButton.icon( + style: fingerprintPrimary + ? AppTheme.primaryOutlinedButtonStyle(context) + : null, + label: const Text('Add fingerprint'), + icon: const Icon(Icons.fingerprint), + onPressed: () { showDialog( context: context, - builder: (context) => FidoPinDialog(node.path, state), + builder: (context) => AddFingerprintDialog(node.path), ); }, ), - MenuAction( - text: 'Delete all data', - icon: const Icon(Icons.delete_outline), - action: (context) { - showDialog( - context: context, - builder: (context) => ResetDialog(node), - ); - }, - ), - ]); - }, - ); - } + OutlinedButton.icon( + label: const Text('Options'), + icon: const Icon(Icons.tune), + onPressed: () { + showBottomMenu(context, [ + MenuAction( + text: 'Change PIN', + icon: const Icon(Icons.pin), + action: (context) { + showDialog( + context: context, + builder: (context) => FidoPinDialog(node.path, state), + ); + }, + ), + MenuAction( + text: 'Reset FIDO', + icon: const Icon(Icons.delete), + action: (context) { + showDialog( + context: context, + builder: (context) => ResetDialog(node), + ); + }, + ), + ]); + }, + ), + ]; } diff --git a/lib/management/views/management_screen.dart b/lib/management/views/management_screen.dart index 6388be5a..544b4b28 100755 --- a/lib/management/views/management_screen.dart +++ b/lib/management/views/management_screen.dart @@ -5,7 +5,6 @@ import 'package:collection/collection.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; -import '../../app/views/app_failure_screen.dart'; import '../../app/views/app_loading_screen.dart'; import '../../core/models.dart'; import '../../widgets/responsive_dialog.dart'; @@ -27,8 +26,8 @@ class _CapabilityForm extends StatelessWidget { @override Widget build(BuildContext context) { return Wrap( - spacing: 4.0, - runSpacing: 8.0, + spacing: 8, + runSpacing: 16, children: Capability.values .where((c) => capabilities & c.value != 0) .map((c) => FilterChip( @@ -85,30 +84,41 @@ class _CapabilitiesForm extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (usbCapabilities != 0) + if (usbCapabilities != 0) ...[ const ListTile( leading: Icon(Icons.usb), - title: Text('USB applications'), + title: Text('USB'), + contentPadding: EdgeInsets.only(bottom: 8), + horizontalTitleGap: 0, ), - _CapabilityForm( - capabilities: usbCapabilities, - enabled: enabled[Transport.usb] ?? 0, - onChanged: (value) { - onChanged({...enabled, Transport.usb: value}); - }, - ), - if (nfcCapabilities != 0) + _CapabilityForm( + capabilities: usbCapabilities, + enabled: enabled[Transport.usb] ?? 0, + onChanged: (value) { + onChanged({...enabled, Transport.usb: value}); + }, + ), + ], + if (nfcCapabilities != 0) ...[ + if (usbCapabilities != 0) + const Padding( + padding: EdgeInsets.only(top: 24, bottom: 12), + child: Divider(), + ), const ListTile( leading: Icon(Icons.wifi), - title: Text('NFC applications'), + title: Text('NFC'), + contentPadding: EdgeInsets.only(bottom: 8), + horizontalTitleGap: 0, ), - _CapabilityForm( - capabilities: nfcCapabilities, - enabled: enabled[Transport.nfc] ?? 0, - onChanged: (value) { - onChanged({...enabled, Transport.nfc: value}); - }, - ), + _CapabilityForm( + capabilities: nfcCapabilities, + enabled: enabled[Transport.nfc] ?? 0, + onChanged: (value) { + onChanged({...enabled, Transport.nfc: value}); + }, + ), + ] ], ); } @@ -230,7 +240,18 @@ class _ManagementScreenState extends ConsumerState { final child = ref.watch(managementStateProvider(widget.deviceData.node.path)).when( loading: () => const AppLoadingScreen(), - error: (error, _) => AppFailureScreen('$error'), + error: (error, _) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + error.toString(), + textAlign: TextAlign.center, + ), + ], + ), + ), data: (info) { bool hasConfig = info.version.major > 4; if (hasConfig) { diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index 8199a1ed..3e9a9a3e 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app/models.dart'; import '../../app/state.dart'; +import '../../core/models.dart'; import '../../core/state.dart'; import '../../widgets/dialog_frame.dart'; import '../models.dart'; @@ -44,17 +46,38 @@ class AccountDialog extends ConsumerWidget with AccountMixin { return deleted; } + Pair _getColors(BuildContext context, MenuAction action) { + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return action.text.startsWith('Copy') + ? Pair(theme.primary, theme.onPrimary) + : (action.text.startsWith('Delete') + ? Pair(theme.error, theme.onError) + : Pair(theme.secondary, theme.onSecondary)); + } + List _buildActions(BuildContext context, WidgetRef ref) { return buildActions(context, ref).map((e) { final action = e.action; - return IconButton( - icon: e.icon, - tooltip: e.text, - onPressed: action != null - ? () { - action(context); - } - : null, + final colors = _getColors(context, e); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: CircleAvatar( + // TODO: Hardcoded color + backgroundColor: action != null ? colors.first : Colors.grey.shade900, + foregroundColor: colors.second, + child: IconButton( + icon: e.icon, + iconSize: 22, + tooltip: e.text, + disabledColor: Colors.white70, + onPressed: action != null + ? () { + action(context); + } + : null, + ), + ), ); }).toList(); } @@ -69,20 +92,62 @@ class AccountDialog extends ConsumerWidget with AccountMixin { } return DialogFrame( child: AlertDialog( - title: Text(title), - contentPadding: const EdgeInsets.symmetric(horizontal: 24.0), - actionsAlignment: MainAxisAlignment.center, - actionsPadding: EdgeInsets.zero, + title: Center( + child: Text( + title, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.headlineSmall, + maxLines: 1, + softWrap: false, + ), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), content: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text(subtitle ?? ''), - const SizedBox(height: 8.0), - Center(child: FittedBox(child: buildCodeView(ref, big: true))), + if (subtitle != null) + Text( + subtitle!, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + softWrap: false, + ), + const SizedBox(height: 12.0), + DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: CardTheme.of(context).color, + borderRadius: const BorderRadius.all(Radius.circular(30.0)), + ), + child: Center( + child: FittedBox( + child: DefaultTextStyle.merge( + style: const TextStyle(fontSize: 28), + child: IconTheme( + data: IconTheme.of(context).copyWith(size: 24), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: buildCodeView(ref), + ), + ), + ), + ), + ), + ), ], ), - actions: [FittedBox(child: Row(children: _buildActions(context, ref)))], + actionsPadding: const EdgeInsets.only(top: 10.0, right: -16.0), + actions: [ + Center( + child: FittedBox( + alignment: Alignment.center, + child: Row(children: _buildActions(context, ref)), + ), + ) + ], ), ); } diff --git a/lib/oath/views/account_list.dart b/lib/oath/views/account_list.dart index 59343723..78590b07 100755 --- a/lib/oath/views/account_list.dart +++ b/lib/oath/views/account_list.dart @@ -94,9 +94,11 @@ class _AccountListState extends ConsumerState { return Column( children: [ if (pinnedCreds.isNotEmpty) - const ListTile( + ListTile( + minVerticalPadding: 16, title: Text( - 'Pinned', + 'PINNED', + style: Theme.of(context).textTheme.labelSmall, ), ), ...pinnedCreds.map( @@ -106,9 +108,11 @@ class _AccountListState extends ConsumerState { ), ), if (creds.isNotEmpty) - const ListTile( + ListTile( + minVerticalPadding: 16, title: Text( - 'Accounts', + 'ACCOUNTS', + style: Theme.of(context).textTheme.labelSmall, ), ), ...creds.map( diff --git a/lib/oath/views/account_mixin.dart b/lib/oath/views/account_mixin.dart index 5a80d6d3..e1aaeff0 100755 --- a/lib/oath/views/account_mixin.dart +++ b/lib/oath/views/account_mixin.dart @@ -147,6 +147,16 @@ mixin AccountMixin { final pinned = isPinned(ref); return [ + MenuAction( + text: 'Copy to clipboard', + icon: const Icon(Icons.copy), + action: code == null || expired + ? null + : (context) { + Clipboard.setData(ClipboardData(text: code.value)); + showMessage(context, 'Code copied to clipboard'); + }, + ), if (manual) MenuAction( text: 'Calculate', @@ -157,27 +167,19 @@ mixin AccountMixin { } : null, ), - MenuAction( - text: 'Copy to clipboard', - icon: const Icon(Icons.copy), - action: code == null || expired - ? null - : (context) { - Clipboard.setData(ClipboardData(text: code.value)); - showMessage(context, 'Code copied to clipboard'); - }, - ), MenuAction( text: pinned ? 'Unpin account' : 'Pin account', //TODO: Replace this with a custom icon. icon: pinned - ? CustomPaint( - painter: _StrikethroughPainter( - Theme.of(context).iconTheme.color ?? Colors.black), - child: ClipPath( - clipper: _StrikethroughClipper(), - child: const Icon(Icons.push_pin_outlined)), - ) + ? Builder(builder: (context) { + return CustomPaint( + painter: _StrikethroughPainter( + IconTheme.of(context).color ?? Colors.black), + child: ClipPath( + clipper: _StrikethroughClipper(), + child: const Icon(Icons.push_pin)), + ); + }) : const Icon(Icons.push_pin_outlined), action: (context) { ref.read(favoritesProvider.notifier).toggleFavorite(credential.id); @@ -194,7 +196,7 @@ mixin AccountMixin { ), MenuAction( text: 'Delete account', - icon: const Icon(Icons.delete_outlined), + icon: const Icon(Icons.delete_outline), action: (context) async { await deleteCredential(context, ref); }, @@ -203,69 +205,59 @@ mixin AccountMixin { } @protected - Widget buildCodeView(WidgetRef ref, {bool big = false}) { + Widget buildCodeView(WidgetRef ref) { final code = getCode(ref); final expired = isExpired(code, ref); - return DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(30.0)), - border: Border.all(width: 1.0, color: Colors.grey.shade500), - ), - child: AnimatedSize( - alignment: Alignment.centerRight, - duration: const Duration(milliseconds: 100), - child: Padding( - padding: big - ? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0) - : const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: code == null - ? [ - Icon( - credential.oathType == OathType.hotp - ? Icons.refresh - : Icons.touch_app, - size: big ? 36 : 18, - ), - Text('', style: TextStyle(fontSize: big ? 32.0 : 22.0)), - ] - : [ - if (credential.oathType == OathType.totp) ...[ - ...expired - ? [ - if (credential.touchRequired) ...[ - const Icon(Icons.touch_app), - const SizedBox(width: 8.0), - ] - ] - : [ - SizedBox.square( - dimension: big ? 32 : 16, - child: CircleTimer( - code.validFrom * 1000, - code.validTo * 1000, - ), - ), + return AnimatedSize( + alignment: Alignment.centerRight, + duration: const Duration(milliseconds: 100), + child: Builder(builder: (context) { + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: code == null + ? [ + Icon( + credential.oathType == OathType.hotp + ? Icons.refresh + : Icons.touch_app, + ), + const Text(''), + ] + : [ + if (credential.oathType == OathType.totp) ...[ + ...expired + ? [ + if (credential.touchRequired) ...[ + const Icon(Icons.touch_app), const SizedBox(width: 8.0), - ], - ], - Opacity( - opacity: expired ? 0.4 : 1.0, - child: Text( - formatCode(code), - style: TextStyle( - fontSize: big ? 32.0 : 22.0, - fontFeatures: const [FontFeature.tabularFigures()], - ), + ] + ] + : [ + SizedBox.square( + dimension: + (IconTheme.of(context).size ?? 18) * 0.8, + child: CircleTimer( + code.validFrom * 1000, + code.validTo * 1000, + ), + ), + const SizedBox(width: 8.0), + ], + ], + Opacity( + opacity: expired ? 0.4 : 1.0, + child: Text( + formatCode(code), + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + //fontWeight: FontWeight.w400, ), ), - ], - ), - ), - ), + ), + ], + ); + }), ); } } diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 8a9320be..a2ceb36e 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -88,56 +88,80 @@ class AccountView extends ConsumerWidget with AccountMixin { items: _buildPopupMenu(context, ref), ); }, - child: ListTile( - focusNode: focusNode, - onTap: () { - showDialog( - context: context, - builder: (context) { - return AccountDialog(credential); - }, - ); - }, - onLongPress: () async { - if (calculateReady) { - await calculateCode( - context, - ref, + child: LayoutBuilder(builder: (context, constraints) { + final showAvatar = constraints.maxWidth >= 315; + return ListTile( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + focusNode: focusNode, + onTap: () { + showDialog( + context: context, + builder: (context) { + return AccountDialog(credential); + }, ); - } - await ref.read(withContextProvider)( - (context) async { - copyToClipboard(context, ref); - }, - ); - }, - leading: CircleAvatar( - foregroundColor: darkMode ? Colors.black : Colors.white, - backgroundColor: _iconColor(darkMode ? 300 : 400), - child: Text( - (credential.issuer ?? credential.name) - .characters - .first - .toUpperCase(), - style: const TextStyle(fontSize: 18), + }, + onLongPress: () async { + if (calculateReady) { + await calculateCode( + context, + ref, + ); + } + await ref.read(withContextProvider)( + (context) async { + copyToClipboard(context, ref); + }, + ); + }, + leading: showAvatar + ? CircleAvatar( + foregroundColor: darkMode ? Colors.black : Colors.white, + backgroundColor: _iconColor(darkMode ? 300 : 400), + child: Text( + (credential.issuer ?? credential.name) + .characters + .first + .toUpperCase(), + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w300), + ), + ) + : null, + title: Text( + title, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.headlineSmall, + maxLines: 1, + softWrap: false, ), - ), - title: Text( - title, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false, - ), - subtitle: subtitle != null - ? Text( - subtitle!, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false, - ) - : null, - trailing: buildCodeView(ref), - ), + subtitle: subtitle != null + ? Text( + subtitle!, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + softWrap: false, + ) + : null, + trailing: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: CardTheme.of(context).color, + borderRadius: const BorderRadius.all(Radius.circular(30.0)), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0), + child: DefaultTextStyle.merge( + style: Theme.of(context).textTheme.bodyLarge, + child: buildCodeView(ref), + ), + ), + ), + ); + }), ); } } diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 4f2ce7b6..730b6325 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -196,7 +196,7 @@ class _OathAddAccountPageState extends ConsumerState { children: [ Text( 'Account details', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), TextField( key: const Key('issuer'), @@ -280,7 +280,7 @@ class _OathAddAccountPageState extends ConsumerState { const Divider(), Text( 'Options', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), Wrap( crossAxisAlignment: WrapCrossAlignment.center, @@ -297,6 +297,7 @@ class _OathAddAccountPageState extends ConsumerState { }, ), Chip( + backgroundColor: ChipTheme.of(context).selectedColor, label: DropdownButtonHideUnderline( child: DropdownButton( value: _oathType, @@ -319,6 +320,7 @@ class _OathAddAccountPageState extends ConsumerState { ), ), Chip( + backgroundColor: ChipTheme.of(context).selectedColor, label: DropdownButtonHideUnderline( child: DropdownButton( value: _hashAlgorithm, @@ -342,6 +344,7 @@ class _OathAddAccountPageState extends ConsumerState { ), if (_oathType == OathType.totp) Chip( + backgroundColor: ChipTheme.of(context).selectedColor, label: DropdownButtonHideUnderline( child: DropdownButton( value: int.tryParse(_periodController.text) ?? @@ -366,6 +369,7 @@ class _OathAddAccountPageState extends ConsumerState { ), ), Chip( + backgroundColor: ChipTheme.of(context).selectedColor, label: DropdownButtonHideUnderline( child: DropdownButton( value: _digits, diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index dc9183f9..d09dcd3f 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -64,7 +64,7 @@ class _ManagePasswordDialogState extends ConsumerState { if (widget.state.hasKey) ...[ Text( 'Current password', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), TextField( autofocus: true, @@ -120,7 +120,7 @@ class _ManagePasswordDialogState extends ConsumerState { ], Text( 'New password', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), TextField( autofocus: !widget.state.hasKey, diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index 35e2ab8a..236c457e 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -6,10 +6,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; -import '../../app/views/app_failure_screen.dart'; +import '../../app/views/app_failure_page.dart'; import '../../app/views/app_loading_screen.dart'; import '../../app/views/app_page.dart'; +import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; +import '../../theme.dart'; import '../models.dart'; import '../state.dart'; import 'account_list.dart'; @@ -29,10 +31,9 @@ class OathScreen extends ConsumerWidget { centered: true, child: const AppLoadingScreen(), ), - error: (error, _) => AppPage( + error: (error, _) => AppFailurePage( title: const Text('Authenticator'), - centered: true, - child: AppFailureScreen('$error'), + cause: error, ), data: (oathState) => oathState.locked ? _LockedView(devicePath, oathState) @@ -50,12 +51,42 @@ class _LockedView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => AppPage( title: const Text('Authenticator'), + actions: [ + OutlinedButton.icon( + label: const Text('Options'), + icon: const Icon(Icons.tune), + onPressed: () { + showBottomMenu(context, [ + MenuAction( + text: 'Manage password', + icon: const Icon(Icons.password), + action: (context) { + showDialog( + context: context, + builder: (context) => + ManagePasswordDialog(devicePath, oathState), + ); + }, + ), + MenuAction( + text: 'Reset OATH', + icon: const Icon(Icons.delete), + action: (context) { + showDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }, + ), + ]); + }, + ), + ], child: Column( children: [ const ListTile(title: Text('Unlock')), _UnlockForm( devicePath, - oathState, keystore: oathState.keystore, ), ], @@ -76,9 +107,9 @@ class _UnlockedView extends ConsumerWidget { if (isEmpty) { return MessagePage( title: const Text('Authenticator'), + graphic: noAccounts, header: 'No accounts', - message: 'Follow the instructions on a website to add an account', - floatingActionButton: _buildFab(context), + actions: _buildActions(context, true), ); } @@ -96,6 +127,7 @@ class _UnlockedView extends ConsumerWidget { return TextFormField( key: const Key('search_accounts'), initialValue: ref.read(searchProvider), + style: Theme.of(context).textTheme.titleSmall, decoration: const InputDecoration( hintText: 'Search accounts', border: InputBorder.none, @@ -110,64 +142,64 @@ class _UnlockedView extends ConsumerWidget { ); }), ), - floatingActionButton: _buildFab(context), + actions: _buildActions(context, false), child: AccountList(devicePath, oathState), ); } - FloatingActionButton _buildFab(BuildContext context) { - final fab = FloatingActionButton.extended( - icon: const Icon(Icons.person_add_alt_1), - label: const Text('Setup'), - onPressed: () { - showBottomMenu(context, [ - MenuAction( - text: 'Add account', - icon: const Icon(Icons.person_add_alt), - action: (context) { - showDialog( - context: context, - builder: (context) => OathAddAccountPage( - devicePath, - openQrScanner: Platform.isAndroid, - ), - ); - }, - ), - MenuAction( - text: oathState.hasKey ? 'Manage password' : 'Set password', - icon: const Icon(Icons.password), - action: (context) { - showDialog( - context: context, - builder: (context) => - ManagePasswordDialog(devicePath, oathState), - ); - }, - ), - MenuAction( - text: 'Reset OATH', - icon: const Icon(Icons.delete_outline), - action: (context) { - showDialog( - context: context, - builder: (context) => ResetDialog(devicePath), - ); - }, - ), - ]); - }, - ); - return fab; + List _buildActions(BuildContext context, bool isEmpty) { + return [ + OutlinedButton.icon( + style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null, + label: const Text('Add account'), + icon: const Icon(Icons.person_add_alt_1), + onPressed: () { + showDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + openQrScanner: Platform.isAndroid, + ), + ); + }, + ), + OutlinedButton.icon( + label: const Text('Options'), + icon: const Icon(Icons.tune), + onPressed: () { + showBottomMenu(context, [ + MenuAction( + text: oathState.hasKey ? 'Manage password' : 'Set password', + icon: const Icon(Icons.password), + action: (context) { + showDialog( + context: context, + builder: (context) => + ManagePasswordDialog(devicePath, oathState), + ); + }, + ), + MenuAction( + text: 'Reset OATH', + icon: const Icon(Icons.delete), + action: (context) { + showDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }, + ), + ]); + }, + ), + ]; } } class _UnlockForm extends ConsumerStatefulWidget { final DevicePath _devicePath; - final OathState _oathState; final KeystoreState keystore; - const _UnlockForm(this._devicePath, this._oathState, - {required this.keystore}); + const _UnlockForm(this._devicePath, {required this.keystore}); @override ConsumerState<_UnlockForm> createState() => _UnlockFormState(); @@ -224,33 +256,6 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> { onChanged: (_) => setState(() {}), // Update state on change onSubmitted: (_) => _submit(), ), - Wrap( - spacing: 4.0, - runSpacing: 8.0, - children: [ - OutlinedButton.icon( - icon: const Icon(Icons.password), - label: const Text('Manage password'), - onPressed: () { - showDialog( - context: context, - builder: (context) => ManagePasswordDialog( - widget._devicePath, widget._oathState), - ); - }, - ), - OutlinedButton.icon( - icon: const Icon(Icons.delete_outlined), - label: const Text('Reset OATH'), - onPressed: () { - showDialog( - context: context, - builder: (context) => ResetDialog(widget._devicePath), - ); - }, - ), - ], - ), ], ), ), diff --git a/lib/theme.dart b/lib/theme.dart index 0281506a..adb354bf 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -const primaryGreen = Color(0xffa8c86c); +const primaryGreen = Color(0xffaed581); const accentGreen = Color(0xff9aca3c); const primaryBlue = Color(0xff325f74); +const primaryRed = Color(0xffea4335); class AppTheme { static ThemeData get lightTheme => ThemeData( @@ -14,26 +15,71 @@ class AppTheme { secondary: accentGreen, background: Colors.grey.shade200, ), - backgroundColor: Colors.white, + iconTheme: IconThemeData( + color: Colors.grey.shade400, + size: 18.0, + ), + //backgroundColor: Colors.white, toggleableActiveColor: accentGreen, appBarTheme: AppBarTheme( - elevation: 0.5, - backgroundColor: Colors.white, + elevation: 0, + toolbarHeight: 48, + //shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + backgroundColor: Colors.transparent, foregroundColor: Colors.grey.shade800, ), + // Mainly used for the OATH dialog view at the moment + buttonTheme: ButtonThemeData( + colorScheme: ColorScheme.light( + secondary: Colors.grey.shade300, + onSecondary: Colors.grey.shade900, + primary: primaryGreen, + onPrimary: Colors.grey.shade900, + error: primaryRed, + onError: Colors.grey.shade100, + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + primary: Colors.grey.shade800, + side: BorderSide(width: 1, color: Colors.grey.shade400), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + )), + cardTheme: CardTheme( + color: Colors.grey.shade300, + ), + chipTheme: ChipThemeData( + backgroundColor: Colors.transparent, + selectedColor: const Color(0xffd2dbdf), + side: BorderSide(width: 1, color: Colors.grey.shade400), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), floatingActionButtonTheme: const FloatingActionButtonThemeData( backgroundColor: primaryBlue, ), - textTheme: TextTheme( - bodyText1: TextStyle( - color: Colors.grey.shade600, - ), - bodyText2: TextStyle( - color: Colors.grey.shade800, - ), - headline2: TextStyle( - color: Colors.grey.shade800, - ), + fontFamily: 'Roboto', + textTheme: const TextTheme( + //bodySmall: TextStyle(color: Colors.grey.shade500), + //bodyLarge: const TextStyle(color: Colors.white70), + //bodyMedium: TextStyle(color: Colors.grey.shade200), + //labelSmall: TextStyle(color: Colors.grey.shade500), + //labelMedium: TextStyle(color: Colors.cyan.shade200), + //labelLarge: TextStyle(color: Colors.cyan.shade500), + //titleSmall: TextStyle(color: Colors.grey.shade600), + //titleMedium: const TextStyle(), + titleLarge: TextStyle( + //color: Colors.grey.shade500, + fontWeight: FontWeight.w400, + fontSize: 18), + headlineSmall: TextStyle( + //color: Colors.grey.shade200, + fontWeight: FontWeight.w300, + fontSize: 16), ), ); @@ -43,23 +89,85 @@ class AppTheme { colorScheme: ColorScheme.fromSwatch(brightness: Brightness.dark).copyWith( primary: primaryGreen, - secondary: primaryGreen, + onPrimary: Colors.black, + secondary: const Color(0xff5d7d90), + ), + iconTheme: const IconThemeData( + color: Colors.white70, + size: 18.0, ), toggleableActiveColor: primaryGreen, + appBarTheme: AppBarTheme( + elevation: 0, + toolbarHeight: 48, + //shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + backgroundColor: Colors.transparent, + foregroundColor: Colors.grey.shade400, + ), + // Mainly used for the OATH dialog view at the moment + buttonTheme: ButtonThemeData( + colorScheme: ColorScheme.dark( + secondary: Colors.grey.shade800, + onSecondary: Colors.white70, + primary: primaryGreen, + onPrimary: Colors.grey.shade900, + error: primaryRed, + onError: Colors.grey.shade100, + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + primary: Colors.white70, + side: const BorderSide(width: 1, color: Colors.white12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + )), + cardTheme: CardTheme( + color: Colors.grey.shade800, + ), + chipTheme: ChipThemeData( + backgroundColor: Colors.transparent, + selectedColor: Colors.white12, + side: const BorderSide(width: 1, color: Colors.white12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + dialogTheme: const DialogTheme( + backgroundColor: Color(0xff323232), + ), floatingActionButtonTheme: FloatingActionButtonThemeData( foregroundColor: Colors.grey.shade900, backgroundColor: primaryGreen, ), + fontFamily: 'Roboto', textTheme: TextTheme( - bodyText1: TextStyle( - color: Colors.grey.shade400, - ), - bodyText2: TextStyle( - color: Colors.grey.shade500, - ), - headline2: TextStyle( - color: Colors.grey.shade100, - ), + bodySmall: TextStyle(color: Colors.grey.shade500), + bodyLarge: const TextStyle(color: Colors.white70), + bodyMedium: TextStyle(color: Colors.grey.shade200), + labelSmall: TextStyle(color: Colors.grey.shade500), + labelMedium: TextStyle(color: Colors.cyan.shade200), + labelLarge: TextStyle(color: Colors.cyan.shade500), + titleSmall: TextStyle(color: Colors.grey.shade600), + titleMedium: const TextStyle(), + titleLarge: TextStyle( + color: Colors.grey.shade500, + fontWeight: FontWeight.w400, + fontSize: 18), + headlineSmall: TextStyle( + color: Colors.grey.shade200, + fontWeight: FontWeight.w300, + fontSize: 16), ), ); + + static ButtonStyle primaryOutlinedButtonStyle(BuildContext context) => + OutlinedButton.styleFrom( + primary: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + side: + BorderSide(width: 1, color: Theme.of(context).colorScheme.primary), + ); } diff --git a/lib/widgets/circle_timer.dart b/lib/widgets/circle_timer.dart index f37ed6de..73251181 100755 --- a/lib/widgets/circle_timer.dart +++ b/lib/widgets/circle_timer.dart @@ -59,6 +59,8 @@ class _CircleTimerState extends State @override Widget build(BuildContext context) { - return ProgressCircle(Colors.grey, _progress.value); + return ProgressCircle( + Theme.of(context).iconTheme.color ?? Colors.grey.shade600, + _progress.value); } } diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 0f671db5..b5febfe9 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -51,7 +51,6 @@ class _ResponsiveDialogState extends State { : 'Cancel'; return DialogFrame( child: AlertDialog( - insetPadding: EdgeInsets.zero, title: widget.title, scrollable: true, content: SizedBox( diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png old mode 100755 new mode 100644 index 3f95a778..da5e8283 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 148ba27a..869a2c83 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png old mode 100755 new mode 100644 index 7e5ff3e3..af386372 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png old mode 100755 new mode 100644 index 209fb65f..f1eb1bdc Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png old mode 100755 new mode 100644 index 0a589662..0ac1a6ba Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png old mode 100755 new mode 100644 index b6f795dd..a35bd96d Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png old mode 100755 new mode 100644 index 1fc02190..6980373a Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.yaml b/pubspec.yaml index 4a6cb245..c8d80ee7 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,7 +88,7 @@ flutter: # - images/a_dot_ham.jpeg assets: - assets/product-images/ - + - assets/graphics/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -115,3 +115,13 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + + fonts: + - family: Roboto + fonts: + - asset: assets/fonts/Roboto-Regular.ttf + weight: 400 + - asset: assets/fonts/Roboto-Light.ttf + weight: 300 + - asset: assets/fonts/Roboto-Thin.ttf + weight: 100